From cca199795db999deac34ead38bb38dd85552070b Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:51:01 +0530 Subject: [PATCH] refactor: migrate app metadata --- .../drift_schemas/main/drift_schema_v33.json | 3740 +++++++++++++++++ .../background_sync_teardown_test.dart | 4 - .../lib/domain/models/app_metadata_key.dart | 20 + mobile/lib/domain/models/store.model.dart | 8 +- mobile/lib/domain/models/value_codec.dart | 6 +- mobile/lib/domain/services/asset.service.dart | 7 +- .../services/background_worker.service.dart | 3 + .../domain/services/local_sync.service.dart | 7 +- .../domain/services/sync_stream.service.dart | 39 +- .../entities/app_metadata.entity.dart | 18 + .../repositories/app_metadata.repository.dart | 28 + .../repositories/db.repository.dart | 12 +- .../repositories/db.repository.steps.dart | 617 +++ .../presentation/actions/delete.action.dart | 6 +- .../presentation/widgets/map/map.state.dart | 3 - .../lib/providers/app_settings.provider.dart | 4 - .../infrastructure/app_metadata.provider.dart | 7 + .../infrastructure/asset.provider.dart | 2 + .../infrastructure/sync.provider.dart | 3 + mobile/lib/services/app_settings.service.dart | 23 - .../utils/hooks/app_settings_update_hook.dart | 15 - mobile/lib/utils/migration.dart | 185 +- .../lib/widgets/forms/login/login_form.dart | 11 +- .../widgets/settings/advanced_settings.dart | 24 +- .../asset_list_group_settings.dart | 2 - .../asset_list_layout_settings.dart | 4 - .../asset_list_settings.dart | 2 - .../image_viewer_quality_setting.dart | 2 - .../sync_status_and_actions.dart | 30 +- .../services/local_sync_service_test.dart | 15 +- .../domain/services/store_service_test.dart | 8 +- .../services/sync_stream_service_test.dart | 34 +- mobile/test/drift/main/migration_test.dart | 41 +- .../repositories/store_repository_test.dart | 24 +- .../test/infrastructure/repository.mock.dart | 3 + .../app_metadata_repository_test.dart | 90 + mobile/test/service.mocks.dart | 5 - mobile/test/unit/mocks.dart | 6 + .../actions/delete_action_test.dart | 7 +- .../presentation/presentation_context.dart | 2 + .../unit/services/asset_service_test.dart | 1 + 41 files changed, 4825 insertions(+), 243 deletions(-) create mode 100644 mobile/drift_schemas/main/drift_schema_v33.json create mode 100644 mobile/lib/domain/models/app_metadata_key.dart create mode 100644 mobile/lib/infrastructure/entities/app_metadata.entity.dart create mode 100644 mobile/lib/infrastructure/repositories/app_metadata.repository.dart delete mode 100644 mobile/lib/providers/app_settings.provider.dart create mode 100644 mobile/lib/providers/infrastructure/app_metadata.provider.dart delete mode 100644 mobile/lib/services/app_settings.service.dart delete mode 100644 mobile/lib/utils/hooks/app_settings_update_hook.dart create mode 100644 mobile/test/medium/repositories/app_metadata_repository_test.dart diff --git a/mobile/drift_schemas/main/drift_schema_v33.json b/mobile/drift_schemas/main/drift_schema_v33.json new file mode 100644 index 0000000000..983e76b7c8 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v33.json @@ -0,0 +1,3740 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": true + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "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": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "remote_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "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": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "local_date_time", + "getter_name": "localDateTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "thumb_hash", + "getter_name": "thumbHash", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "uploaded_at", + "getter_name": "uploadedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "live_photo_video_id", + "getter_name": "livePhotoVideoId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "visibility", + "getter_name": "visibility", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetVisibility.values)", + "dart_type_name": "AssetVisibility" + } + }, + { + "name": "stack_id", + "getter_name": "stackId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "library_id", + "getter_name": "libraryId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_edited", + "getter_name": "isEdited", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_edited\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "stack_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "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('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "primary_asset_id", + "getter_name": "primaryAssetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "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": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "i_cloud_id", + "getter_name": "iCloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "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": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'\\'')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "thumbnail_asset_id", + "getter_name": "thumbnailAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "is_activity_enabled", + "getter_name": "isActivityEnabled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "order", + "getter_name": "order", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)", + "dart_type_name": "AlbumAssetOrder" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [ + 4 + ], + "type": "table", + "data": { + "name": "local_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "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": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "backup_selection", + "getter_name": "backupSelection", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(BackupSelection.values)", + "dart_type_name": "BackupSelection" + } + }, + { + "name": "is_ios_shared_album", + "getter_name": "isIosSharedAlbum", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "linked_remote_album_id", + "getter_name": "linkedRemoteAlbumId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [ + 3, + 5 + ], + "type": "table", + "data": { + "name": "local_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 7, + "references": [ + 6 + ], + "type": "index", + "data": { + "on": 6, + "name": "idx_local_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 8, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 10, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_created_at", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 2 + ], + "type": "index", + "data": { + "on": 2, + "name": "idx_stack_primary_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 12, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 13, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_library_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 14, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 15, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_stack_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 16, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n", + "unique": false, + "columns": [] + } + }, + { + "id": 17, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_uploaded", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)", + "unique": false, + "columns": [] + } + }, + { + "id": 18, + "references": [], + "type": "table", + "data": { + "name": "auth_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "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": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_admin", + "getter_name": "isAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + }, + { + "name": "quota_size_in_bytes", + "getter_name": "quotaSizeInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quota_usage_in_bytes", + "getter_name": "quotaUsageInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pin_code", + "getter_name": "pinCode", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 19, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_metadata_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "key", + "getter_name": "key", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)", + "dart_type_name": "UserMetadataKey" + } + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "userMetadataConverter", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "user_id", + "key" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "partner_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shared_by_id", + "getter_name": "sharedById", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "shared_with_id", + "getter_name": "sharedWithId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "in_timeline", + "getter_name": "inTimeline", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"in_timeline\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"in_timeline\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "shared_by_id", + "shared_with_id" + ] + } + }, + { + "id": 21, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_exif_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "city", + "getter_name": "city", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state", + "getter_name": "state", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "country", + "getter_name": "country", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "date_time_original", + "getter_name": "dateTimeOriginal", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "exposure_time", + "getter_name": "exposureTime", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "f_number", + "getter_name": "fNumber", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "file_size", + "getter_name": "fileSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "focal_length", + "getter_name": "focalLength", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "iso", + "getter_name": "iso", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "make", + "getter_name": "make", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "model", + "getter_name": "model", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lens", + "getter_name": "lens", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "time_zone", + "getter_name": "timeZone", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "rating", + "getter_name": "rating", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "projection_type", + "getter_name": "projectionType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 22, + "references": [ + 1, + 4 + ], + "type": "table", + "data": { + "name": "remote_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 23, + "references": [ + 4, + 0 + ], + "type": "table", + "data": { + "name": "remote_album_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "role", + "getter_name": "role", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)", + "dart_type_name": "AlbumUserRole" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "album_id", + "user_id" + ] + } + }, + { + "id": 24, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_asset_cloud_id_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "cloud_id", + "getter_name": "cloudId", + "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": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 25, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "memory_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "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('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(MemoryTypeEnum.values)", + "dart_type_name": "MemoryTypeEnum" + } + }, + { + "name": "data", + "getter_name": "data", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_saved", + "getter_name": "isSaved", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_saved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "memory_at", + "getter_name": "memoryAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "seen_at", + "getter_name": "seenAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "show_at", + "getter_name": "showAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "hide_at", + "getter_name": "hideAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 26, + "references": [ + 1, + 25 + ], + "type": "table", + "data": { + "name": "memory_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "memory_id", + "getter_name": "memoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "memory_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "memory_id" + ] + } + }, + { + "id": 27, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "person_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "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('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "face_asset_id", + "getter_name": "faceAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "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": null, + "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": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "color", + "getter_name": "color", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "birth_date", + "getter_name": "birthDate", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 28, + "references": [ + 1, + 27 + ], + "type": "table", + "data": { + "name": "asset_face_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "person_id", + "getter_name": "personId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "person_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "image_width", + "getter_name": "imageWidth", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "image_height", + "getter_name": "imageHeight", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x1", + "getter_name": "boundingBoxX1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y1", + "getter_name": "boundingBoxY1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x2", + "getter_name": "boundingBoxX2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y2", + "getter_name": "boundingBoxY2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source_type", + "getter_name": "sourceType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 29, + "references": [], + "type": "table", + "data": { + "name": "store_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "string_value", + "getter_name": "stringValue", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "int_value", + "getter_name": "intValue", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 30, + "references": [], + "type": "table", + "data": { + "name": "trashed_local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "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": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source", + "getter_name": "source", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TrashOrigin.values)", + "dart_type_name": "TrashOrigin" + } + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id", + "album_id" + ] + } + }, + { + "id": 31, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_edit_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "action", + "getter_name": "action", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetEditAction.values)", + "dart_type_name": "AssetEditAction" + } + }, + { + "name": "parameters", + "getter_name": "parameters", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "editParameterConverter", + "dart_type_name": "Map" + } + }, + { + "name": "sequence", + "getter_name": "sequence", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 32, + "references": [], + "type": "table", + "data": { + "name": "settings", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 33, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_ocr_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "x1", + "getter_name": "x1", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y1", + "getter_name": "y1", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x2", + "getter_name": "x2", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y2", + "getter_name": "y2", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x3", + "getter_name": "x3", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y3", + "getter_name": "y3", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x4", + "getter_name": "x4", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y4", + "getter_name": "y4", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "box_score", + "getter_name": "boxScore", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "text_score", + "getter_name": "textScore", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recognized_text", + "getter_name": "recognizedText", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 34, + "references": [], + "type": "table", + "data": { + "name": "session", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 35, + "references": [], + "type": "table", + "data": { + "name": "app_metadata", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 36, + "references": [ + 20 + ], + "type": "index", + "data": { + "on": 20, + "name": "idx_partner_shared_with_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 37, + "references": [ + 21 + ], + "type": "index", + "data": { + "on": 21, + "name": "idx_lat_lng", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)", + "unique": false, + "columns": [] + } + }, + { + "id": 38, + "references": [ + 21 + ], + "type": "index", + "data": { + "on": 21, + "name": "idx_remote_exif_city", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 39, + "references": [ + 22 + ], + "type": "index", + "data": { + "on": 22, + "name": "idx_remote_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 40, + "references": [ + 24 + ], + "type": "index", + "data": { + "on": 24, + "name": "idx_remote_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 41, + "references": [ + 27 + ], + "type": "index", + "data": { + "on": 27, + "name": "idx_person_owner_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 42, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_person_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 43, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 44, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_visible_person", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 45, + "references": [ + 30 + ], + "type": "index", + "data": { + "on": 30, + "name": "idx_trashed_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 46, + "references": [ + 30 + ], + "type": "index", + "data": { + "on": 30, + "name": "idx_trashed_local_asset_album", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 47, + "references": [ + 31 + ], + "type": "index", + "data": { + "on": 31, + "name": "idx_asset_edit_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 48, + "references": [ + 33 + ], + "type": "index", + "data": { + "on": 33, + "name": "idx_asset_ocr_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"uploaded_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "stack_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_local_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_local_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)" + } + ] + }, + { + "name": "idx_local_asset_created_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)" + } + ] + }, + { + "name": "idx_stack_primary_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_library_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)" + } + ] + }, + { + "name": "idx_remote_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_remote_asset_stack_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)" + } + ] + }, + { + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)" + } + ] + }, + { + "name": "idx_remote_asset_uploaded", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)" + } + ] + }, + { + "name": "auth_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "user_metadata_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "partner_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_exif_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_cloud_id_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "person_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_face_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "store_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "trashed_local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_edit_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "settings", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"settings\" (\"key\" TEXT NOT NULL, \"value\" TEXT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_ocr_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_ocr_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"x1\" REAL NOT NULL, \"y1\" REAL NOT NULL, \"x2\" REAL NOT NULL, \"y2\" REAL NOT NULL, \"x3\" REAL NOT NULL, \"y3\" REAL NOT NULL, \"x4\" REAL NOT NULL, \"y4\" REAL NOT NULL, \"box_score\" REAL NOT NULL, \"text_score\" REAL NOT NULL, \"recognized_text\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "session", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"session\" (\"key\" TEXT NOT NULL, \"value\" TEXT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "app_metadata", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"app_metadata\" (\"key\" TEXT NOT NULL, \"value\" TEXT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_partner_shared_with_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)" + } + ] + }, + { + "name": "idx_lat_lng", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)" + } + ] + }, + { + "name": "idx_remote_exif_city", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL" + } + ] + }, + { + "name": "idx_remote_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_remote_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)" + } + ] + }, + { + "name": "idx_person_owner_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)" + } + ] + }, + { + "name": "idx_asset_face_person_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)" + } + ] + }, + { + "name": "idx_asset_face_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_face_visible_person", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL" + } + ] + }, + { + "name": "idx_trashed_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_trashed_local_asset_album", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)" + } + ] + }, + { + "name": "idx_asset_edit_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_ocr_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)" + } + ] + } + ] +} \ No newline at end of file diff --git a/mobile/integration_test/background_sync_teardown_test.dart b/mobile/integration_test/background_sync_teardown_test.dart index 6231a36d97..aa0a8a9a8a 100644 --- a/mobile/integration_test/background_sync_teardown_test.dart +++ b/mobile/integration_test/background_sync_teardown_test.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:drift/drift.dart' show Value; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/main.dart' as app; @@ -34,13 +32,11 @@ void main() { server = await FakeImmichServer.start(); await ApiService().resolveAndSetEndpoint(server.endpoint); await drift.delete(drift.userEntity).go(); - await Store.delete(StoreKey.syncMigrationStatus); }); tearDown(() async { await workerManagerPatch.dispose(); await server.close(); - await Store.delete(StoreKey.syncMigrationStatus); }); void sendUser(SyncStream stream, String id, String name) { diff --git a/mobile/lib/domain/models/app_metadata_key.dart b/mobile/lib/domain/models/app_metadata_key.dart new file mode 100644 index 0000000000..c557683ff7 --- /dev/null +++ b/mobile/lib/domain/models/app_metadata_key.dart @@ -0,0 +1,20 @@ +import 'package:immich_mobile/domain/models/value_codec.dart'; + +const int kCurrentVersion = 29; + +enum AppMetadataKey { + version(kCurrentVersion), + syncMigrationStatus>([], codec: ListCodec(PrimitiveCodec.string)), + manageLocalMediaAndroid(false); + + const AppMetadataKey(this.defaultValue, {ValueCodec? codec}) : _codecOverride = codec; + + final T defaultValue; + + final ValueCodec? _codecOverride; + ValueCodec get _codec => _codecOverride ?? ValueCodec.forType(T); + + String encode(T value) => _codec.encode(value); + + T decode(String raw) => _codec.decode(raw); +} diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 2c4042b044..80934d7d46 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -1,14 +1,12 @@ /// Key for each possible value in the `Store`. /// Defines the data type for each value enum StoreKey { - version._(0), deviceId._(4), - manageLocalMediaAndroid._(137), - - syncMigrationStatus._(1013), - // Legacy keys that have been migrated to the new metadata store + legacyVersion._(0), + legacyManageLocalMediaAndroid._(137), + legacySyncMigrationStatus._(1013), legacyAdvancedTroubleshooting._(114), legacyEnableHapticFeedback._(126), legacyReadonlyModeEnabled._(138), diff --git a/mobile/lib/domain/models/value_codec.dart b/mobile/lib/domain/models/value_codec.dart index 814ef1b926..7f7cff2697 100644 --- a/mobile/lib/domain/models/value_codec.dart +++ b/mobile/lib/domain/models/value_codec.dart @@ -116,19 +116,19 @@ final class ListCodec extends ValueCodec> { try { final decoded = jsonDecode(raw); if (decoded is! List) { - return []; + return const []; } final result = []; for (final item in decoded) { if (item is! String) { - return []; + return const []; } final element = _elementCodec.decode(item); result.add(element); } return result; } on FormatException { - return []; + return const []; } } } diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 59f41f1e51..4c56041d4d 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -2,9 +2,8 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart'; @@ -21,6 +20,7 @@ class AssetService { final AssetApiRepository _apiRepository; final AssetMediaRepository _mediaRepository; final DriftTrashedLocalAssetRepository _trashedLocalRepository; + final AppMetadataRepository _appMetadataRepository; const AssetService({ required this._remoteRepository, @@ -29,6 +29,7 @@ class AssetService { required this._apiRepository, required this._mediaRepository, required this._trashedLocalRepository, + required this._appMetadataRepository, }); Future getAsset(BaseAsset asset) { @@ -186,7 +187,7 @@ class AssetService { return 0; } - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + if (CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid)) { await _trashedLocalRepository.applyTrashedAssets(deletedIds); } else { await _localRepository.delete(deletedIds); diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 8161df5c51..fcc54c1020 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -83,6 +84,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { trashedLocalAssetRepository: ref.read(trashedLocalAssetRepository), assetMediaRepository: ref.read(assetMediaRepositoryProvider), permissionRepository: ref.read(permissionRepositoryProvider), + appMetadataRepository: ref.read(appMetadataRepositoryProvider), cancellation: _cancellationToken, ); _remoteSyncService = SyncStreamService( @@ -94,6 +96,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { permissionRepository: ref.read(permissionRepositoryProvider), syncMigrationRepository: ref.read(syncMigrationRepositoryProvider), api: ref.read(apiServiceProvider), + appMetadataRepository: ref.read(appMetadataRepositoryProvider), cancellation: _cancellationToken, ); _hashService = HashService( diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index b4ebc66f89..7de52c329e 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -5,9 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; @@ -28,6 +27,7 @@ class LocalSyncService { final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final AssetMediaRepository _assetMediaRepository; final IPermissionRepository _permissionRepository; + final AppMetadataRepository _appMetadataRepository; final Completer? _cancellation; final Logger _log = Logger("DeviceSyncService"); @@ -38,6 +38,7 @@ class LocalSyncService { required this._trashedLocalAssetRepository, required this._assetMediaRepository, required this._permissionRepository, + required this._appMetadataRepository, this._cancellation, }) { unawaited(_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning))); @@ -48,7 +49,7 @@ class LocalSyncService { Future sync({bool full = false}) async { final Stopwatch stopwatch = Stopwatch()..start(); try { - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + if (CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid)) { final hasPermission = await _permissionRepository.hasManageMediaPermission(); if (hasPermission) { await _syncTrashedAssets(); diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 9b4d4bb275..1540775578 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,13 +1,11 @@ // ignore_for_file: constant_identifier_names import 'dart:async'; -import 'dart:convert'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; @@ -39,6 +37,7 @@ class SyncStreamService { final IPermissionRepository _permissionRepository; final SyncMigrationRepository _syncMigrationRepository; final ApiService _api; + final AppMetadataRepository _appMetadataRepository; final Completer? _cancellation; SyncStreamService({ @@ -50,10 +49,12 @@ class SyncStreamService { required this._permissionRepository, required this._syncMigrationRepository, required this._api, + required this._appMetadataRepository, this._cancellation, }); bool get isCancelled => _cancellation?.isCompleted ?? false; + bool _manageLocalMediaAndroid = false; Future sync() async { _logger.info("Remote sync request for user"); @@ -65,16 +66,17 @@ class SyncStreamService { final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); - final value = Store.get(StoreKey.syncMigrationStatus, "[]"); - final migrations = (jsonDecode(value) as List).cast(); + final migrations = (await _appMetadataRepository.get(.syncMigrationStatus)).toList(); int previousLength = migrations.length; await _runPreSyncTasks(migrations, serverSemVer); if (migrations.length != previousLength) { _logger.info("Updated pre-sync migration status: $migrations"); - await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); + await _appMetadataRepository.set(.syncMigrationStatus, migrations); } + _manageLocalMediaAndroid = CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid); + // Start the sync stream and handle events bool shouldReset = false; await _syncApiRepository.streamChanges( @@ -97,7 +99,7 @@ class SyncStreamService { if (migrations.length != previousLength) { _logger.info("Updated pre-sync migration status: $migrations"); - await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); + await _appMetadataRepository.set(.syncMigrationStatus, migrations); } return true; @@ -113,10 +115,10 @@ class SyncStreamService { if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) { _logger.info("Running pre-sync task: v20260128_ResetExifV1"); await _syncApiRepository.deleteSyncAck([ - SyncEntityType.assetExifV1, - SyncEntityType.partnerAssetExifV1, - SyncEntityType.albumAssetExifCreateV1, - SyncEntityType.albumAssetExifUpdateV1, + .assetExifV1, + .partnerAssetExifV1, + .albumAssetExifCreateV1, + .albumAssetExifUpdateV1, ]); migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name); } @@ -124,12 +126,7 @@ class SyncStreamService { if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) && semVer >= const SemVer(major: 2, minor: 5, patch: 0)) { _logger.info("Running pre-sync task: v20260128_ResetAssetV1"); - await _syncApiRepository.deleteSyncAck([ - SyncEntityType.assetV1, - SyncEntityType.partnerAssetV1, - SyncEntityType.albumAssetCreateV1, - SyncEntityType.albumAssetUpdateV1, - ]); + await _syncApiRepository.deleteSyncAck([.assetV1, .partnerAssetV1, .albumAssetCreateV1, .albumAssetUpdateV1]); migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name); @@ -141,7 +138,7 @@ class SyncStreamService { if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) && semVer > const SemVer(major: 2, minor: 7, patch: 5)) { _logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2"); - await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]); + await _syncApiRepository.deleteSyncAck([.assetV1, .assetV2]); migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name); } } @@ -204,20 +201,20 @@ class SyncStreamService { case SyncEntityType.assetV1: final remoteSyncAssets = data.cast(); await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + if (_manageLocalMediaAndroid) { await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList()); } return; case SyncEntityType.assetV2: final remoteSyncAssets = data.cast(); await _syncStreamRepository.updateAssetsV2(remoteSyncAssets); - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + if (_manageLocalMediaAndroid) { await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList()); } return; case SyncEntityType.assetDeleteV1: final remoteSyncAssets = data.cast(); - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + if (_manageLocalMediaAndroid) { await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList()); } return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets); diff --git a/mobile/lib/infrastructure/entities/app_metadata.entity.dart b/mobile/lib/infrastructure/entities/app_metadata.entity.dart new file mode 100644 index 0000000000..0f22d4d7fe --- /dev/null +++ b/mobile/lib/infrastructure/entities/app_metadata.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AppMetadataEntity extends Table with DriftDefaultsMixin { + const AppMetadataEntity(); + + TextColumn get key => text()(); + + TextColumn get value => text().nullable()(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {key}; + + @override + String get tableName => "app_metadata"; +} diff --git a/mobile/lib/infrastructure/repositories/app_metadata.repository.dart b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart new file mode 100644 index 0000000000..7f8faa6bb5 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart @@ -0,0 +1,28 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; +import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class AppMetadataRepository { + final Drift _db; + + const AppMetadataRepository(this._db); + + Future get(AppMetadataKey key) async { + final row = await (_db.select(_db.appMetadataEntity)..where((row) => row.key.equals(key.name))).getSingleOrNull(); + final value = row?.value; + return value == null ? key.defaultValue : key.decode(value); + } + + Future set(AppMetadataKey key, U value) async { + await _db + .into(_db.appMetadataEntity) + .insertOnConflictUpdate( + AppMetadataEntityCompanion.insert( + key: key.name, + value: Value(key.encode(value)), + updatedAt: Value(DateTime.now()), + ), + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index d2873ed49e..0bd5242cd2 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -6,6 +6,7 @@ import 'package:drift/drift.dart'; import 'package:drift/src/runtime/executor/stream_queries.dart' show StreamQueryStore; import 'package:drift_sqlite_async/drift_sqlite_async.dart'; import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.dart'; @@ -69,6 +70,7 @@ import 'package:sqlite_async/sqlite_async.dart'; SettingsEntity, AssetOcrEntity, SessionEntity, + AppMetadataEntity, ], include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, ) @@ -122,7 +124,7 @@ class Drift extends $Drift { } @override - int get schemaVersion => 32; + int get schemaVersion => 33; @override MigrationStrategy get migration => MigrationStrategy( @@ -323,6 +325,14 @@ class Drift extends $Drift { from31To32: (m, v32) async { await m.createTable(v32.session); }, + from32To33: (m, v33) async { + await m.createTable(v33.appMetadata); + await customStatement( + "INSERT INTO app_metadata (key, value) " + "SELECT 'version', CAST(int_value AS TEXT) FROM store_entity " + "WHERE id = 0 AND int_value IS NOT NULL", + ); + }, ), ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 8104ba04c8..1e16d1a932 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -17103,6 +17103,615 @@ final class Schema32 extends i0.VersionedSchema { ); } +final class Schema33 extends i0.VersionedSchema { + Schema33({required super.database}) : super(version: 33); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxLocalAssetCreatedAt, + idxStackPrimaryAssetId, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetOwnerVisibilityDeletedCreated, + idxRemoteAssetUploaded, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + assetEditEntity, + settings, + assetOcrEntity, + session, + appMetadata, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteExifCity, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxAssetFaceVisiblePerson, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + idxAssetEditAssetId, + idxAssetOcrAssetId, + ]; + late final Shape33 userEntity = Shape33( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape50 remoteAssetEntity = Shape50( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_212, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 stackEntity = Shape35( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_130, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape36 localAssetEntity = Shape36( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_131, + _column_120, + _column_132, + _column_133, + _column_134, + _column_135, + _column_136, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 remoteAlbumEntity = Shape48( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_138, + _column_114, + _column_115, + _column_139, + _column_140, + _column_141, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 localAlbumEntity = Shape38( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_115, + _column_142, + _column_143, + _column_144, + _column_145, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape39 localAlbumAssetEntity = Shape39( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_146, _column_147, _column_145], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxLocalAssetCreatedAt = i1.Index( + 'idx_local_asset_created_at', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index( + 'idx_remote_asset_owner_visibility_deleted_created', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)', + ); + final i1.Index idxRemoteAssetUploaded = i1.Index( + 'idx_remote_asset_uploaded', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)', + ); + late final Shape40 authUserEntity = Shape40( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_108, + _column_109, + _column_148, + _column_110, + _column_111, + _column_149, + _column_150, + _column_151, + _column_152, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_153, _column_154, _column_155], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 partnerEntity = Shape41( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_156, _column_157, _column_158], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape42 remoteExifEntity = Shape42( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_160, + _column_161, + _column_162, + _column_163, + _column_164, + _column_117, + _column_116, + _column_165, + _column_166, + _column_167, + _column_168, + _column_135, + _column_136, + _column_169, + _column_170, + _column_171, + _column_172, + _column_173, + _column_174, + _column_175, + _column_176, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_159, _column_177], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_177, _column_153, _column_178], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 remoteAssetCloudIdEntity = Shape43( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_159, + _column_179, + _column_180, + _column_134, + _column_135, + _column_136, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 memoryEntity = Shape44( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_124, + _column_121, + _column_113, + _column_181, + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_159, _column_187], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 personEntity = Shape45( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_114, + _column_115, + _column_121, + _column_108, + _column_188, + _column_189, + _column_190, + _column_191, + _column_192, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 assetFaceEntity = Shape46( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_193, + _column_194, + _column_195, + _column_196, + _column_197, + _column_198, + _column_199, + _column_200, + _column_201, + _column_124, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_202, _column_203, _column_204], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 trashedLocalAssetEntity = Shape47( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_108, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_107, + _column_205, + _column_131, + _column_120, + _column_132, + _column_206, + _column_137, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 assetEditEntity = Shape32( + source: i0.VersionedTable( + entityName: 'asset_edit_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_207, + _column_208, + _column_209, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 settings = Shape49( + source: i0.VersionedTable( + entityName: 'settings', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_210, _column_224, _column_115], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape51 assetOcrEntity = Shape51( + source: i0.VersionedTable( + entityName: 'asset_ocr_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_107, + _column_159, + _column_213, + _column_214, + _column_215, + _column_216, + _column_217, + _column_218, + _column_219, + _column_220, + _column_221, + _column_222, + _column_223, + _column_201, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 session = Shape49( + source: i0.VersionedTable( + entityName: 'session', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_210, _column_224, _column_115], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape49 appMetadata = Shape49( + source: i0.VersionedTable( + entityName: 'app_metadata', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY("key")'], + columns: [_column_210, _column_224, _column_115], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteExifCity = i1.Index( + 'idx_remote_exif_city', + 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxAssetFaceVisiblePerson = i1.Index( + 'idx_asset_face_visible_person', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + final i1.Index idxAssetEditAssetId = i1.Index( + 'idx_asset_edit_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)', + ); + final i1.Index idxAssetOcrAssetId = i1.Index( + 'idx_asset_ocr_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -17135,6 +17744,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema30 schema) from29To30, required Future Function(i1.Migrator m, Schema31 schema) from30To31, required Future Function(i1.Migrator m, Schema32 schema) from31To32, + required Future Function(i1.Migrator m, Schema33 schema) from32To33, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -17293,6 +17903,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from31To32(migrator, schema); return 32; + case 32: + final schema = Schema33(database: database); + final migrator = i1.Migrator(database, schema); + await from32To33(migrator, schema); + return 33; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -17331,6 +17946,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema30 schema) from29To30, required Future Function(i1.Migrator m, Schema31 schema) from30To31, required Future Function(i1.Migrator m, Schema32 schema) from31To32, + required Future Function(i1.Migrator m, Schema33 schema) from32To33, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -17364,5 +17980,6 @@ i1.OnUpgrade stepByStep({ from29To30: from29To30, from30To31: from30To31, from31To32: from31To32, + from32To33: from32To33, ), ); diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index 31d01b32ef..e3f35b4314 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -209,9 +209,9 @@ Future _cleanupLocalAssets( final requiresPrompt = requestCustomPrompt && CurrentPlatform.isAndroid && - ref.read(storeServiceProvider).get(.manageLocalMediaAndroid, false); + await ref.read(appMetadataRepositoryProvider).get(.manageLocalMediaAndroid); - if (requiresPrompt) { + if (requiresPrompt && context.mounted) { final confirmed = await showDialog( context: context, builder: (_) => ConfirmDialog( diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index eedf3aadf5..421e0af220 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -81,9 +81,6 @@ class MapStateNotifier extends Notifier { } void switchTheme(ThemeMode mode) { - // TODO: Remove this line when map theme provider is removed - // Until then, keep both in sync as MapThemeOverride uses map state provider - // ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index); ref.read(mapStateNotifierProvider.notifier).switchTheme(mode); state = state.copyWith(themeMode: mode); } diff --git a/mobile/lib/providers/app_settings.provider.dart b/mobile/lib/providers/app_settings.provider.dart deleted file mode 100644 index 3d3947a931..0000000000 --- a/mobile/lib/providers/app_settings.provider.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; - -final appSettingsServiceProvider = Provider((_) => const AppSettingsService()); diff --git a/mobile/lib/providers/infrastructure/app_metadata.provider.dart b/mobile/lib/providers/infrastructure/app_metadata.provider.dart new file mode 100644 index 0000000000..9830ee9c53 --- /dev/null +++ b/mobile/lib/providers/infrastructure/app_metadata.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final appMetadataRepositoryProvider = Provider( + (ref) => AppMetadataRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index 8ca0ca0ec9..cc6f0911bf 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_asset.repository import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; @@ -31,6 +32,7 @@ final assetServiceProvider = Provider( apiRepository: ref.watch(assetApiRepositoryProvider), mediaRepository: ref.watch(assetMediaRepositoryProvider), trashedLocalRepository: ref.watch(trashedLocalAssetRepository), + appMetadataRepository: ref.read(appMetadataRepositoryProvider), ), ); diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index 700b51f12d..f7b3ff01f1 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_migration.reposit import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -26,6 +27,7 @@ final syncStreamServiceProvider = Provider( permissionRepository: ref.watch(permissionRepositoryProvider), syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider), api: ref.watch(apiServiceProvider), + appMetadataRepository: ref.watch(appMetadataRepositoryProvider), cancellation: ref.watch(cancellationProvider), ), ); @@ -42,6 +44,7 @@ final localSyncServiceProvider = Provider( assetMediaRepository: ref.watch(assetMediaRepositoryProvider), permissionRepository: ref.watch(permissionRepositoryProvider), nativeSyncApi: ref.watch(nativeSyncApiProvider), + appMetadataRepository: ref.watch(appMetadataRepositoryProvider), cancellation: ref.watch(cancellationProvider), ), ); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart deleted file mode 100644 index 4a8b76b1ce..0000000000 --- a/mobile/lib/services/app_settings.service.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; - -enum AppSettingsEnum { - manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false); - - const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); - - final StoreKey storeKey; - final String? hiveKey; - final T defaultValue; -} - -class AppSettingsService { - const AppSettingsService(); - T getSetting(AppSettingsEnum setting) { - return Store.get(setting.storeKey, setting.defaultValue); - } - - Future setSetting(AppSettingsEnum setting, T value) { - return Store.put(setting.storeKey, value); - } -} diff --git a/mobile/lib/utils/hooks/app_settings_update_hook.dart b/mobile/lib/utils/hooks/app_settings_update_hook.dart deleted file mode 100644 index 9d7cc4b064..0000000000 --- a/mobile/lib/utils/hooks/app_settings_update_hook.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; - -ValueNotifier useAppSettingsState(AppSettingsEnum key) { - final notifier = useState(Store.get(key.storeKey, key.defaultValue)); - - // Listen to changes to the notifier and update app settings - useValueChanged(notifier.value, (_, __) => unawaited(Store.put(key.storeKey, notifier.value))); - - return notifier; -} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 1ab3b2902f..ab190acdd1 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -6,6 +6,7 @@ import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; @@ -14,8 +15,10 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/feature_message.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; @@ -23,11 +26,13 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -const int targetVersion = 28; - Future migrateDatabaseIfNeeded(Drift drift) async { - final int? storedVersion = Store.tryGet(StoreKey.version); - final version = storedVersion ?? targetVersion; + final isFreshInstall = await drift.appMetadataEntity + .count(where: (row) => row.key.equals(AppMetadataKey.version.name)) + .map((count) => count == 0) + .getSingle(); + final metadata = AppMetadataRepository(drift); + final version = await metadata.get(.version); if (version < 25) { await _migrateTo25(); @@ -37,9 +42,6 @@ Future migrateDatabaseIfNeeded(Drift drift) async { await _migrateTo26(drift); } - if (storedVersion == null) { - await FeatureMessageService(SettingsRepository.instance).markSeen(); - } if (version < 27) { await _migrateTo27(drift); } @@ -48,26 +50,34 @@ Future migrateDatabaseIfNeeded(Drift drift) async { await _migrateTo28(drift); } - await Store.put(StoreKey.version, targetVersion); + if (version < 29) { + await _migrateTo29(drift); + } + + if (isFreshInstall) { + await FeatureMessageService(SettingsRepository.instance).markSeen(); + } + + await metadata.set(.version, kCurrentVersion); return; } Future _migrateTo25() async { - final accessToken = Store.tryGet(StoreKey.legacyAccessToken); + final accessToken = Store.tryGet(.legacyAccessToken); if (accessToken == null || accessToken.isEmpty) { return; } final urls = []; - final serverEndpoint = Store.tryGet(StoreKey.legacyServerEndpoint); + final serverEndpoint = Store.tryGet(.legacyServerEndpoint); if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final localEndpoint = Store.tryGet(StoreKey.legacyLocalEndpoint); + final localEndpoint = Store.tryGet(.legacyLocalEndpoint); if (localEndpoint != null && localEndpoint.isNotEmpty) { urls.add(localEndpoint); } - final externalJson = Store.tryGet(StoreKey.legacyExternalEndpointList); + final externalJson = Store.tryGet(.legacyExternalEndpointList); if (externalJson != null) { final List list = jsonDecode(externalJson); for (final entry in list) { @@ -81,7 +91,7 @@ Future _migrateTo25() async { return; } - final customHeadersStr = Store.get(StoreKey.legacyCustomHeaders, ""); + final customHeadersStr = Store.get(.legacyCustomHeaders, ""); final headers = customHeadersStr.isEmpty ? const {} : (jsonDecode(customHeadersStr) as Map).cast(); @@ -91,75 +101,67 @@ Future _migrateTo25() async { Future _migrateTo26(Drift drift) async { final migrator = _StoreMigrator.settings(drift); - await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values); + await migrator.migrateEnumIndex(.legacyLogLevel, .logLevel, LogLevel.values); // Theme - await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values); - await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, SettingsKey.themePrimaryColor, ImmichColorPreset.values); - await migrator.migrateBool(StoreKey.legacyDynamicTheme, SettingsKey.themeDynamic); - await migrator.migrateBool(StoreKey.legacyColorfulInterface, SettingsKey.themeColorfulInterface); + await migrator.migrateEnumName(.legacyThemeMode, .themeMode, ThemeMode.values); + await migrator.migrateEnumName(.legacyPrimaryColor, .themePrimaryColor, ImmichColorPreset.values); + await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic); + await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface); // Cleanup - final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); + final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); - migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, SettingsKey.cleanupKeepAlbumIds, ids); + migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids); } - await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, SettingsKey.cleanupKeepFavorites); - await migrator.migrateEnumIndex( - StoreKey.legacyCleanupKeepMediaType, - SettingsKey.cleanupKeepMediaType, - AssetKeepType.values, - ); - await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, SettingsKey.cleanupCutoffDaysAgo); - await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, SettingsKey.cleanupDefaultsInitialized); + await migrator.migrateBool(.legacyCleanupKeepFavorites, .cleanupKeepFavorites); + await migrator.migrateEnumIndex(.legacyCleanupKeepMediaType, .cleanupKeepMediaType, AssetKeepType.values); + await migrator.migrateInt(.legacyCleanupCutoffDaysAgo, .cleanupCutoffDaysAgo); + await migrator.migrateBool(.legacyCleanupDefaultsInitialized, .cleanupDefaultsInitialized); // Map - await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, SettingsKey.mapShowFavoriteOnly); - await migrator.migrateInt(StoreKey.legacyMapRelativeDate, SettingsKey.mapRelativeDate); - await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, SettingsKey.mapIncludeArchived); - await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, SettingsKey.mapThemeMode, ThemeMode.values); - await migrator.migrateBool(StoreKey.legacyMapwithPartners, SettingsKey.mapWithPartners); + await migrator.migrateBool(.legacyMapShowFavoriteOnly, .mapShowFavoriteOnly); + await migrator.migrateInt(.legacyMapRelativeDate, .mapRelativeDate); + await migrator.migrateBool(.legacyMapIncludeArchived, .mapIncludeArchived); + await migrator.migrateEnumIndex(.legacyMapThemeMode, .mapThemeMode, ThemeMode.values); + await migrator.migrateBool(.legacyMapwithPartners, .mapWithPartners); // Timeline - await migrator.migrateInt(StoreKey.legacyTilesPerRow, SettingsKey.timelineTilesPerRow); - await migrator.migrateEnumIndex( - StoreKey.legacyGroupAssetsBy, - SettingsKey.timelineGroupAssetsBy, - GroupAssetsBy.values, - ); - await migrator.migrateBool(StoreKey.legacyStorageIndicator, SettingsKey.timelineStorageIndicator); + await migrator.migrateInt(.legacyTilesPerRow, .timelineTilesPerRow); + await migrator.migrateEnumIndex(.legacyGroupAssetsBy, .timelineGroupAssetsBy, GroupAssetsBy.values); + await migrator.migrateBool(.legacyStorageIndicator, .timelineStorageIndicator); // Image - await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, SettingsKey.imagePreferRemote); - await migrator.migrateBool(StoreKey.legacyLoadOriginal, SettingsKey.imageLoadOriginal); + await migrator.migrateBool(.legacyPreferRemoteImage, .imagePreferRemote); + await migrator.migrateBool(.legacyLoadOriginal, .imageLoadOriginal); // Viewer - await migrator.migrateBool(StoreKey.legacyLoopVideo, SettingsKey.viewerLoopVideo); - await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, SettingsKey.viewerLoadOriginalVideo); - await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, SettingsKey.viewerAutoPlayVideo); - await migrator.migrateBool(StoreKey.legacyTapToNavigate, SettingsKey.viewerTapToNavigate); + await migrator.migrateBool(.legacyLoopVideo, .viewerLoopVideo); + await migrator.migrateBool(.legacyLoadOriginalVideo, .viewerLoadOriginalVideo); + await migrator.migrateBool(.legacyAutoPlayVideo, .viewerAutoPlayVideo); + await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate); // Network - await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, SettingsKey.networkAutoEndpointSwitching); - final preferredWifiName = await migrator.readLegacyStoreString(StoreKey.legacyPreferredWifiName.id); - migrator.stage(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName, preferredWifiName); - final localEndpoint = await migrator.readLegacyStoreString(StoreKey.legacyLocalEndpoint.id); - migrator.stage(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint, localEndpoint); + await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching); + final preferredWifiName = await migrator.readLegacyStoreString(.legacyPreferredWifiName); + migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName); + final localEndpoint = await migrator.readLegacyStoreString(.legacyLocalEndpoint); + migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint); await _migrateExternalEndpointList(migrator); await _migrateCustomHeaders(migrator); // Album await _migrateAlbumSortMode(migrator); - await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, SettingsKey.albumIsReverse); - await migrator.migrateBool(StoreKey.legacyAlbumGridView, SettingsKey.albumIsGrid); + await migrator.migrateBool(.legacySelectedAlbumSortReverse, .albumIsReverse); + await migrator.migrateBool(.legacyAlbumGridView, .albumIsGrid); // Backup - await migrator.migrateBool(StoreKey.legacyEnableBackup, SettingsKey.backupEnabled); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, SettingsKey.backupUseCellularForVideos); - await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, SettingsKey.backupUseCellularForPhotos); - await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, SettingsKey.backupRequireCharging); - await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, SettingsKey.backupTriggerDelay); - await migrator.migrateBool(StoreKey.legacySyncAlbums, SettingsKey.backupSyncAlbums); + await migrator.migrateBool(.legacyEnableBackup, .backupEnabled); + await migrator.migrateBool(.legacyUseWifiForUploadVideos, .backupUseCellularForVideos); + await migrator.migrateBool(.legacyUseWifiForUploadPhotos, .backupUseCellularForPhotos); + await migrator.migrateBool(.legacyBackupRequireCharging, .backupRequireCharging); + await migrator.migrateInt(.legacyBackupTriggerDelay, .backupTriggerDelay); + await migrator.migrateBool(.legacySyncAlbums, .backupSyncAlbums); await migrator.complete(); } Future _migrateTo27(Drift drift) async { final migrator = _StoreMigrator.session(drift); - await migrator.migrateString(StoreKey.legacyServerUrl, SessionKey.serverUrl); - await migrator.migrateString(StoreKey.legacyAccessToken, SessionKey.accessToken); - await migrator.migrateString(StoreKey.legacyServerEndpoint, SessionKey.serverEndpoint); + await migrator.migrateString(.legacyServerUrl, .serverUrl); + await migrator.migrateString(.legacyAccessToken, .accessToken); + await migrator.migrateString(.legacyServerEndpoint, .serverEndpoint); await migrator.complete(); await SessionRepository.instance.refresh(); @@ -167,26 +169,40 @@ Future _migrateTo27(Drift drift) async { Future _migrateTo28(Drift drift) async { final migrator = _StoreMigrator.settings(drift); - await migrator.migrateBool(StoreKey.legacyAdvancedTroubleshooting, SettingsKey.advancedTroubleshooting); - await migrator.migrateBool(StoreKey.legacyEnableHapticFeedback, SettingsKey.advancedEnableHapticFeedback); - await migrator.migrateBool(StoreKey.legacyReadonlyModeEnabled, SettingsKey.advancedReadonlyModeEnabled); + await migrator.migrateBool(.legacyAdvancedTroubleshooting, .advancedTroubleshooting); + await migrator.migrateBool(.legacyEnableHapticFeedback, .advancedEnableHapticFeedback); + await migrator.migrateBool(.legacyReadonlyModeEnabled, .advancedReadonlyModeEnabled); await migrator.complete(); await SettingsRepository.instance.refresh(); } +Future _migrateTo29(Drift drift) async { + final migrator = _StoreMigrator.appMetadata(drift); + + final rawStatus = await migrator.readLegacyStoreString(.legacySyncMigrationStatus); + if (rawStatus != null) { + final decoded = jsonDecode(rawStatus); + final migrations = decoded is List ? decoded.whereType().toList() : []; + migrator.stage(.legacySyncMigrationStatus, .syncMigrationStatus, migrations); + } + + await migrator.migrateBool(.legacyManageLocalMediaAndroid, .manageLocalMediaAndroid); + await migrator.complete(); +} + Future _migrateAlbumSortMode(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id); + final raw = await migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder); final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); if (mode == null) { return; } - migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode); + migrator.stage(.legacySelectedAlbumSortOrder, .albumSortMode, mode); } Future _migrateExternalEndpointList(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id); + final raw = await migrator.readLegacyStoreString(.legacyExternalEndpointList); if (raw == null) { return; } @@ -210,7 +226,7 @@ Future _migrateExternalEndpointList(_StoreMigrator migrator) } Future _migrateCustomHeaders(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id); + final raw = await migrator.readLegacyStoreString(.legacyCustomHeaders); if (raw == null) { return; } @@ -257,6 +273,17 @@ class _StoreMigrator { ), ); + static _StoreMigrator appMetadata(Drift db) => _StoreMigrator._( + db, + encode: (key, value) => key.encode(value), + readDefault: (_) => null, + insertRow: (batch, name, value) => batch.insert( + db.appMetadataEntity, + AppMetadataEntityCompanion(key: Value(name), value: Value(value)), + mode: InsertMode.insertOrReplace, + ), + ); + final Drift _db; final String Function(K key, Object value) encode; final Object? Function(K key) readDefault; @@ -265,7 +292,7 @@ class _StoreMigrator { final List _migratedStoreIds = []; Future migrateEnumIndex(StoreKey legacyKey, K newKey, List values) async { - final index = await readLegacyStoreInt(legacyKey.id); + final index = await readLegacyStoreInt(legacyKey); if (index == null) { return; } @@ -280,7 +307,7 @@ class _StoreMigrator { } Future migrateEnumName(StoreKey legacyKey, K newKey, List values) async { - final name = await readLegacyStoreString(legacyKey.id); + final name = await readLegacyStoreString(legacyKey); if (name == null) { return; } @@ -295,7 +322,7 @@ class _StoreMigrator { } Future migrateBool(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey.id); + final intValue = await readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -305,7 +332,7 @@ class _StoreMigrator { } Future migrateInt(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey.id); + final intValue = await readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -315,7 +342,7 @@ class _StoreMigrator { } Future migrateString(StoreKey legacyKey, K newKey) async { - final value = await readLegacyStoreString(legacyKey.id); + final value = await readLegacyStoreString(legacyKey); if (value == null || value.isEmpty) { return; } @@ -325,7 +352,7 @@ class _StoreMigrator { } Future migrateNullableString(StoreKey legacyKey, K newKey) async { - _cache[newKey] = await readLegacyStoreString(legacyKey.id); + _cache[newKey] = await readLegacyStoreString(legacyKey); _migratedStoreIds.add(legacyKey.id); } @@ -348,13 +375,13 @@ class _StoreMigrator { await deleteLegacyStoreRows(_migratedStoreIds); } - Future readLegacyStoreString(int id) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); + Future readLegacyStoreString(StoreKey key) async { + final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); return row?.stringValue; } - Future readLegacyStoreInt(int id) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); + Future readLegacyStoreInt(StoreKey key) async { + final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); return row?.intValue; } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index e4c7516c43..691fc4476a 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -11,8 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; @@ -20,6 +19,7 @@ import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/feature_message.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; @@ -247,7 +247,8 @@ class LoginForm extends HookConsumerWidget { } } - bool isSyncRemoteDeletionsMode() => Platform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false); + Future isSyncRemoteDeletionsMode() async => + Platform.isAndroid && await ref.read(appMetadataRepositoryProvider).get(AppMetadataKey.manageLocalMediaAndroid); Future login() async { TextInput.finishAutofillContext(); @@ -266,7 +267,7 @@ class LoginForm extends HookConsumerWidget { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { + if (await isSyncRemoteDeletionsMode()) { await getManageMediaPermission(); } unawaited(handleSyncFlow()); @@ -371,7 +372,7 @@ class LoginForm extends HookConsumerWidget { if (isSuccess && context.mounted) { await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); - if (isSyncRemoteDeletionsMode()) { + if (await isSyncRemoteDeletionsMode()) { await getManageMediaPermission(); } unawaited(handleSyncFlow()); diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 5eb104ce32..3f81b17dda 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -6,15 +6,15 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -33,7 +33,7 @@ class AdvancedSettings extends HookConsumerWidget { advancedTroubleshooting.value, (_, __) => unawaited(ref.read(settingsProvider).write(.advancedTroubleshooting, advancedTroubleshooting.value)), ); - final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); + final manageLocalMediaAndroid = useState(false); final isManageMediaSupported = useState(false); final manageMediaAndroidPermission = useState(false); final levelId = useState(ref.watch(appConfigProvider).logLevel.index); @@ -64,7 +64,17 @@ class AdvancedSettings extends HookConsumerWidget { useEffect(() { unawaited(() async { isManageMediaSupported.value = await checkAndroidVersion(); - if (isManageMediaSupported.value && context.mounted) { + if (!context.mounted) { + return; + } + + if (isManageMediaSupported.value) { + manageLocalMediaAndroid.value = await ref + .read(appMetadataRepositoryProvider) + .get(AppMetadataKey.manageLocalMediaAndroid); + if (!context.mounted) { + return; + } manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); } }()); @@ -91,6 +101,12 @@ class AdvancedSettings extends HookConsumerWidget { final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission(); manageLocalMediaAndroid.value = result; manageMediaAndroidPermission.value = result; + if (!context.mounted) { + return; + } + await ref.read(appMetadataRepositoryProvider).set(.manageLocalMediaAndroid, result); + } else { + await ref.read(appMetadataRepositoryProvider).set(.manageLocalMediaAndroid, false); } }, ), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index a5ced2ef5f..f00f3bbd75 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -5,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -20,7 +19,6 @@ class GroupSettings extends HookConsumerWidget { Future updateAppSettings(GroupAssetsBy groupBy) async { await ref.read(settingsProvider).write(.timelineGroupAssetsBy, groupBy); - ref.invalidate(appSettingsServiceProvider); ref.invalidate(timelineServiceProvider); } diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 5b4c4bb89f..93655db8f3 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; @@ -34,9 +33,6 @@ class LayoutSettings extends HookConsumerWidget { maxValue: 6, minValue: 2, noDivisons: 4, - onChangeEnd: (value) { - ref.invalidate(appSettingsServiceProvider); - }, ), ], ); diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 1f3f4bbcf0..60f38d565c 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_settings.dart'; @@ -24,7 +23,6 @@ class AssetListSettings extends HookConsumerWidget { title: 'theme_setting_asset_list_storage_indicator_title'.tr(), onChanged: (value) { unawaited(ref.read(settingsProvider).write(.timelineStorageIndicator, value)); - ref.invalidate(appSettingsServiceProvider); ref.invalidate(settingsProvider); }, ), diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 82a2d804a3..12af688968 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -31,7 +30,6 @@ class ImageViewerQualitySetting extends HookConsumerWidget { valueNotifier: isOriginal, title: "setting_image_viewer_original_title".t(context: context), subtitle: "setting_image_viewer_original_subtitle".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], ); diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 097f72e1e1..a16135d2c2 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -3,13 +3,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @@ -17,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; @@ -234,7 +234,7 @@ class _SyncStatsCounts extends ConsumerWidget { final localAlbumService = ref.watch(localAlbumServiceProvider); final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); final memoryService = ref.watch(driftMemoryServiceProvider); - final appSettingsService = ref.watch(appSettingsServiceProvider); + final appMetadataRepository = ref.watch(appMetadataRepositoryProvider); Future> loadCounts() async { final assetCounts = assetService.getAssetCounts(); @@ -242,8 +242,16 @@ class _SyncStatsCounts extends ConsumerWidget { final remoteAlbumCounts = remoteAlbumService.getCount(); final memoryCount = memoryService.getCount(); final getLocalHashedCount = assetService.getLocalHashedCount(); + final manageLocalMediaAndroid = appMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid); - return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]); + return await Future.wait([ + assetCounts, + localAlbumCounts, + remoteAlbumCounts, + memoryCount, + getLocalHashedCount, + manageLocalMediaAndroid, + ]); } return FutureBuilder( @@ -269,14 +277,15 @@ class _SyncStatsCounts extends ConsumerWidget { ); } - final assetCounts = snapshot.data![0]! as (int, int); + final assetCounts = snapshot.data![0] as (int, int); final localAssetCount = assetCounts.$1; final remoteAssetCount = assetCounts.$2; - final localAlbumCount = snapshot.data![1]! as int; - final remoteAlbumCount = snapshot.data![2]! as int; - final memoryCount = snapshot.data![3]! as int; - final localHashedCount = snapshot.data![4]! as int; + final localAlbumCount = snapshot.data![1] as int; + final remoteAlbumCount = snapshot.data![2] as int; + final memoryCount = snapshot.data![3] as int; + final localHashedCount = snapshot.data![4] as int; + final manageLocalMediaAndroid = snapshot.data![5] as bool; return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -369,8 +378,7 @@ class _SyncStatsCounts extends ConsumerWidget { ), ), // To be removed once the experimental feature is stable - if (CurrentPlatform.isAndroid && - appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ + if (CurrentPlatform.isAndroid && manageLocalMediaAndroid) ...[ SettingGroupTitle(title: "trash".t(context: context)), Consumer( builder: (context, ref, _) { diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 14277709da..cdc45f7d09 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -2,8 +2,8 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -29,6 +29,7 @@ void main() { late AssetMediaRepository mockAssetMediaRepository; late MockPermissionRepository mockPermissionRepository; late MockNativeSyncApi mockNativeSyncApi; + late MockAppMetadataRepository mockAppMetadataRepository; late Drift db; setUpAll(() async { @@ -52,6 +53,7 @@ void main() { mockAssetMediaRepository = MockAssetMediaRepository(); mockPermissionRepository = MockPermissionRepository(); mockNativeSyncApi = MockNativeSyncApi(); + mockAppMetadataRepository = MockAppMetadataRepository(); when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( @@ -75,15 +77,16 @@ void main() { assetMediaRepository: mockAssetMediaRepository, permissionRepository: mockPermissionRepository, nativeSyncApi: mockNativeSyncApi, + appMetadataRepository: mockAppMetadataRepository, ); - await Store.put(StoreKey.manageLocalMediaAndroid, false); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false); }); group('LocalSyncService - syncTrashedAssets gating', () { test('invokes syncTrashedAssets when Android flag enabled and permission granted', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); await sut.sync(); @@ -93,7 +96,7 @@ void main() { }); test('skips syncTrashedAssets when store flag disabled', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, false); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); await sut.sync(); @@ -102,7 +105,7 @@ void main() { }); test('skips syncTrashedAssets when MANAGE_MEDIA permission absent', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false); await sut.sync(); @@ -114,7 +117,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; addTearDown(() => debugDefaultTargetPlatformOverride = TargetPlatform.android); - await Store.put(StoreKey.manageLocalMediaAndroid, true); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); await sut.sync(); diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index f1616aa2eb..ca84fe04a2 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -22,14 +22,14 @@ void main() { mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.legacyAccessToken); - registerFallbackValue(StoreKey.version); + registerFallbackValue(StoreKey.legacyVersion); registerFallbackValue(StoreKey.legacyAdvancedTroubleshooting); when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.legacyAccessToken, _kAccessToken), const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kAdvancedTroubleshooting), - const StoreDto(StoreKey.version, _kVersion), + const StoreDto(StoreKey.legacyVersion, _kVersion), ], ); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); @@ -47,7 +47,7 @@ void main() { verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken); expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), _kAdvancedTroubleshooting); - expect(sut.tryGet(StoreKey.version), _kVersion); + expect(sut.tryGet(StoreKey.legacyVersion), _kVersion); // Other keys should be null expect(sut.tryGet(StoreKey.deviceId), isNull); }); @@ -148,7 +148,7 @@ void main() { verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.legacyAccessToken), isNull); expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), isNull); - expect(sut.tryGet(StoreKey.version), isNull); + expect(sut.tryGet(StoreKey.legacyVersion), isNull); }); }); } diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 5cd4104f91..e6cadda310 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -4,8 +4,8 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; @@ -50,6 +50,7 @@ void main() { late Future Function(List, Function(), Function()) handleEventsCallback; late _MockAbortCallbackWrapper mockAbortCallbackWrapper; late _MockAbortCallbackWrapper mockResetCallbackWrapper; + late MockAppMetadataRepository mockAppMetadataRepository; late Drift db; late bool hasManageMediaPermission; @@ -58,6 +59,8 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; registerFallbackValue(LocalAssetStub.image1); registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0)); + registerFallbackValue(AppMetadataKey.syncMigrationStatus); + registerFallbackValue(const []); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); @@ -83,6 +86,7 @@ void main() { mockApi = MockApiService(); mockServerApi = MockServerApi(); mockSyncMigrationRepo = MockSyncMigrationRepository(); + mockAppMetadataRepository = MockAppMetadataRepository(); when(() => mockAbortCallbackWrapper()).thenReturn(false); @@ -158,6 +162,7 @@ void main() { permissionRepository: mockPermissionRepo, api: mockApi, syncMigrationRepository: mockSyncMigrationRepo, + appMetadataRepository: mockAppMetadataRepository, ); when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {}); @@ -171,7 +176,11 @@ void main() { return ids; }); when(() => mockAssetMediaRepo.restoreAssetsFromTrash(any())).thenAnswer((_) async => []); - await Store.put(StoreKey.manageLocalMediaAndroid, false); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false); + when( + () => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus), + ).thenAnswer((_) async => const []); + when(() => mockAppMetadataRepository.set, List>(any(), any())).thenAnswer((_) async {}); }); Future simulateEvents(List events) async { @@ -242,6 +251,7 @@ void main() { cancellation: cancellation, api: mockApi, syncMigrationRepository: mockSyncMigrationRepo, + appMetadataRepository: mockAppMetadataRepository, ); await sut.sync(); @@ -282,6 +292,7 @@ void main() { cancellation: cancellation, api: mockApi, syncMigrationRepository: mockSyncMigrationRepo, + appMetadataRepository: mockAppMetadataRepository, ); await sut.sync(); @@ -393,12 +404,12 @@ void main() { group("SyncStreamService - remote trash & restore", () { setUp(() async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true); hasManageMediaPermission = true; }); tearDown(() async { - await Store.put(StoreKey.manageLocalMediaAndroid, false); + when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false); hasManageMediaPermission = false; }); @@ -551,7 +562,9 @@ void main() { group('SyncStreamService - Sync Migration', () { test('ensure that <2.5.0 migrations run', () async { - await Store.put(StoreKey.syncMigrationStatus, "[]"); + when( + () => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus), + ).thenAnswer((_) async => const []); when( () => mockServerApi.getServerVersion(), ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); @@ -579,7 +592,9 @@ void main() { ); }); test('ensure that >=2.5.0 migrations run', () async { - await Store.put(StoreKey.syncMigrationStatus, "[]"); + when( + () => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus), + ).thenAnswer((_) async => const []); when( () => mockServerApi.getServerVersion(), ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null)); @@ -605,10 +620,9 @@ void main() { }); test('ensure that migrations do not re-run', () async { - await Store.put( - StoreKey.syncMigrationStatus, - '["${SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name}"]', - ); + when( + () => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus), + ).thenAnswer((_) async => [SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name]); when( () => mockServerApi.getServerVersion(), diff --git a/mobile/test/drift/main/migration_test.dart b/mobile/test/drift/main/migration_test.dart index 74467492ae..c0bedf462e 100644 --- a/mobile/test/drift/main/migration_test.dart +++ b/mobile/test/drift/main/migration_test.dart @@ -1,13 +1,11 @@ -// dart format width=80 -// ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations_native.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'generated/schema.dart'; -import 'generated/schema_v1.dart' as v1; -import 'generated/schema_v2.dart' as v2; +import 'generated/schema_v32.dart' as v32; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -35,4 +33,39 @@ void main() { }); } }); + + group('data migrations', () { + test('v32->v33 backfills the migration', () async { + const version = 28; + final schema = await verifier.schemaAt(32); + + final oldDb = v32.DatabaseAtV32(schema.newConnection()); + await oldDb + .into(oldDb.storeEntity) + .insert(v32.StoreEntityCompanion.insert(id: 0, intValue: const Value(version))); + await oldDb.close(); + + final db = Drift(schema.newConnection()); + await verifier.migrateAndValidate(db, 33); + + final cursor = await (db.appMetadataEntity.select()..where((tbl) => tbl.key.equals(AppMetadataKey.version.name))) + .map((row) => row.value) + .getSingleOrNull(); + expect(cursor, version.toString()); + + await db.close(); + }); + + test('v32->v33 writes no row when the legacy store has none', () async { + final schema = await verifier.schemaAt(32); + + final db = Drift(schema.newConnection()); + await verifier.migrateAndValidate(db, 33); + + final rows = await db.appMetadataEntity.select().get(); + expect(rows, isEmpty); + + await db.close(); + }); + }); } diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 9962f34314..2d3bda5fab 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -33,7 +33,7 @@ Future _populateStore(Drift db) async { batch.insert( db.storeEntity, StoreEntityCompanion( - id: Value(StoreKey.version.id), + id: Value(StoreKey.legacyVersion.id), intValue: const Value(_kTestVersion), stringValue: const Value(null), ), @@ -56,10 +56,10 @@ void main() { group('Store Repository converters:', () { test('converts int', () async { - int? version = await sut.tryGet(StoreKey.version); + int? version = await sut.tryGet(StoreKey.legacyVersion); expect(version, isNull); - await sut.upsert(StoreKey.version, _kTestVersion); - version = await sut.tryGet(StoreKey.version); + await sut.upsert(StoreKey.legacyVersion, _kTestVersion); + version = await sut.tryGet(StoreKey.legacyVersion); expect(version, _kTestVersion); }); @@ -107,10 +107,10 @@ void main() { }); test('upsert()', () async { - int? version = await sut.tryGet(StoreKey.version); + int? version = await sut.tryGet(StoreKey.legacyVersion); expect(version, _kTestVersion); - await sut.upsert(StoreKey.version, _kTestVersion + 10); - version = await sut.tryGet(StoreKey.version); + await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); + version = await sut.tryGet(StoreKey.legacyVersion); expect(version, _kTestVersion + 10); }); }); @@ -121,10 +121,10 @@ void main() { }); test('watch()', () async { - final stream = sut.watch(StoreKey.version); + final stream = sut.watch(StoreKey.legacyVersion); unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]))); await pumpEventQueue(); - await sut.upsert(StoreKey.version, _kTestVersion + 10); + await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); }); test('watchAll()', () async { @@ -134,19 +134,19 @@ void main() { stream, emitsInOrder([ [ - const StoreDto(StoreKey.version, _kTestVersion), + const StoreDto(StoreKey.legacyVersion, _kTestVersion), const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), ], [ - const StoreDto(StoreKey.version, _kTestVersion + 10), + const StoreDto(StoreKey.legacyVersion, _kTestVersion + 10), const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), ], ]), ), ); - await sut.upsert(StoreKey.version, _kTestVersion + 10); + await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); }); }); } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index d9e77f8eeb..c0c511bce0 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; @@ -22,6 +23,8 @@ class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} class MockSettingsRepository extends Mock implements SettingsRepository {} +class MockAppMetadataRepository extends Mock implements AppMetadataRepository {} + class MockLogRepository extends Mock implements LogRepository {} class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} diff --git a/mobile/test/medium/repositories/app_metadata_repository_test.dart b/mobile/test/medium/repositories/app_metadata_repository_test.dart new file mode 100644 index 0000000000..d47cabc9d0 --- /dev/null +++ b/mobile/test/medium/repositories/app_metadata_repository_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/app_metadata_key.dart'; +import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late AppMetadataRepository sut; + + setUpAll(() { + ctx = MediumRepositoryContext(); + sut = AppMetadataRepository(ctx.db); + }); + + tearDownAll(() async { + await ctx.dispose(); + }); + + setUp(() async { + await ctx.db.delete(ctx.db.appMetadataEntity).go(); + }); + + group('get', () { + test('a stored NULL value column resolves to the key default', () async { + await ctx.db + .into(ctx.db.appMetadataEntity) + .insert( + AppMetadataEntityCompanion.insert( + key: AppMetadataKey.manageLocalMediaAndroid.name, + value: const .new(null), + updatedAt: .new(DateTime.now()), + ), + ); + + expect(await sut.get(.manageLocalMediaAndroid), false); + }); + }); + + group('defaults', () { + test('falls back to the key default when the value is absent', () async { + expect(await sut.get(.version), kCurrentVersion); + expect(await sut.get(.syncMigrationStatus), const []); + expect(await sut.get(.manageLocalMediaAndroid), false); + }); + + test('a stored value takes precedence over the default', () async { + await sut.set(.version, 5); + await sut.set(.syncMigrationStatus, const ['task']); + await sut.set(.manageLocalMediaAndroid, true); + + expect(await sut.get(.version), 5); + expect(await sut.get(.syncMigrationStatus), const ['task']); + expect(await sut.get(.manageLocalMediaAndroid), true); + }); + }); + + group('set', () { + test('round-trips int, List and bool values to their typed form', () async { + await sut.set(.version, 42); + await sut.set(.syncMigrationStatus, const ['task']); + await sut.set(.manageLocalMediaAndroid, true); + + expect(await sut.get(.version), 42); + expect(await sut.get(.syncMigrationStatus), const ['task']); + expect(await sut.get(.manageLocalMediaAndroid), true); + }); + + test('overwrites the existing value and keeps a single row per key', () async { + await sut.set(.version, 1); + await sut.set(.version, 2); + + expect(await sut.get(.version), 2); + expect(await ctx.db.select(ctx.db.appMetadataEntity).get(), hasLength(1)); + }); + }); + + group('cache-less reads', () { + test('observes a value mutated directly in the DB', () async { + await sut.set(.version, 10); + + await (ctx.db.update(ctx.db.appMetadataEntity)..where((r) => r.key.equals(AppMetadataKey.version.name))).write( + AppMetadataEntityCompanion(value: .new(AppMetadataKey.version.encode(99))), + ); + + expect(await sut.get(.version), 99); + }); + }); +} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index ecf9c771bc..7e67d4d250 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -7,7 +7,6 @@ import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/cleanup.service.dart'; @@ -24,14 +23,10 @@ class MockApiService extends Mock implements ApiService {} class MockNetworkService extends Mock implements NetworkService {} -class MockAppSettingService extends Mock implements AppSettingsService {} - class MockStoreService extends Mock implements StoreService {} class MockNativeSyncApi extends Mock implements NativeSyncApi {} -class MockAppSettingsService extends Mock implements AppSettingsService {} - class MockPartnerService extends Mock implements PartnerService {} class MockAssetService extends Mock implements AssetService {} diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index 279212ee04..5b314d5b34 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -29,6 +29,7 @@ class RepositoryMocks { final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository()); final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository()); final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository()); + final metadata = MockAppMetadataRepository(); final trashedAsset = MockTrashedLocalAssetRepository(); final remoteAlbum = MockRemoteAlbumRepository(); final albumApi = MockDriftAlbumApiRepository(); @@ -63,6 +64,7 @@ class RepositoryMocks { _stubAssetApiRepository(); _stubAssetMediaRepository(); _stubDownloadRepository(); + _stubAppMetadataRepository(); } void _stubRemoteAssetRepository() { @@ -101,6 +103,10 @@ class RepositoryMocks { void _stubDownloadRepository() { when(download.downloadAllAssets).thenAnswer((_) async => const []); } + + void _stubAppMetadataRepository() { + when(() => metadata.get(.manageLocalMediaAndroid)).thenAnswer((_) async => false); + } } class ServiceMocks { diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart index 29d81a5c2c..99a6520ef9 100644 --- a/mobile/test/unit/presentation/actions/delete_action_test.dart +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; @@ -31,7 +29,6 @@ void main() { tearDown(() async { debugDefaultTargetPlatformOverride = null; - await StoreService.I.put(StoreKey.manageLocalMediaAndroid, false); await context.dispose(); }); @@ -190,7 +187,7 @@ void main() { testWidgets('local only delete on Android with MANAGE_MEDIA shows the prompt', (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.android; - await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + when(() => context.repository.metadata.get(.manageLocalMediaAndroid)).thenAnswer((_) async => true); final asset = LocalAssetFactory.create(); await pumpDelete(tester, {asset}); @@ -207,7 +204,7 @@ void main() { testWidgets('local only delete on Android with MANAGE_MEDIA deletes nothing when cancelled', (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.android; - await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + when(() => context.repository.metadata.get(.manageLocalMediaAndroid)).thenAnswer((_) async => true); final asset = LocalAssetFactory.create(); await pumpDelete(tester, {asset}); diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 89b717003c..fb7e874e78 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart' import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; @@ -60,6 +61,7 @@ class PresentationContext { inLockedViewProvider.overrideWithValue(false), remoteAssetRepositoryProvider.overrideWithValue(repository.remoteAsset.repo), assetMediaRepositoryProvider.overrideWithValue(repository.assetMedia.api), + appMetadataRepositoryProvider.overrideWithValue(repository.metadata), ]; List selected(Set assets) => [ diff --git a/mobile/test/unit/services/asset_service_test.dart b/mobile/test/unit/services/asset_service_test.dart index a09465701b..fc87e3db39 100644 --- a/mobile/test/unit/services/asset_service_test.dart +++ b/mobile/test/unit/services/asset_service_test.dart @@ -26,6 +26,7 @@ void main() { apiRepository: apiRepository, mediaRepository: mocks.assetMedia.api, trashedLocalRepository: mocks.trashedAsset, + appMetadataRepository: mocks.metadata, ); });