diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt index 6ae4c99bd7..b5ef90310e 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -87,7 +87,8 @@ data class PlatformAsset ( val updatedAt: Long? = null, val width: Long? = null, val height: Long? = null, - val durationInSeconds: Long + val durationInSeconds: Long, + val orientation: Long ) { companion object { @@ -100,7 +101,8 @@ data class PlatformAsset ( val width = pigeonVar_list[5] as Long? val height = pigeonVar_list[6] as Long? val durationInSeconds = pigeonVar_list[7] as Long - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds) + val orientation = pigeonVar_list[8] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation) } } fun toList(): List { @@ -113,6 +115,7 @@ data class PlatformAsset ( width, height, durationInSeconds, + orientation, ) } override fun equals(other: Any?): Boolean { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 5183c274b3..02cd54b8c3 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -40,7 +40,8 @@ open class NativeSyncApiImplBase(context: Context) { MediaStore.MediaColumns.BUCKET_ID, MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.HEIGHT, - MediaStore.MediaColumns.DURATION + MediaStore.MediaColumns.DURATION, + MediaStore.MediaColumns.ORIENTATION, ) const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 @@ -74,6 +75,8 @@ open class NativeSyncApiImplBase(context: Context) { val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + val orientationColumn = + c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION) while (c.moveToNext()) { val id = c.getLong(idColumn).toString() @@ -101,6 +104,7 @@ open class NativeSyncApiImplBase(context: Context) { val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else c.getLong(durationColumn) / 1000 val bucketId = c.getString(bucketIdColumn) + val orientation = c.getInt(orientationColumn) val asset = PlatformAsset( id, @@ -110,7 +114,8 @@ open class NativeSyncApiImplBase(context: Context) { modifiedAt, width, height, - duration + duration, + orientation.toLong(), ) yield(AssetResult.ValidAsset(asset, bucketId)) } diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 493a34cc94..4960cd0a0d 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.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":"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":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","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":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"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":[]}],"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_in_seconds","getter_name":"durationInSeconds","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":["unknown"]},{"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":"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"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"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_in_seconds","getter_name":"durationInSeconds","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"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":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":7,"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":["unknown"]},{"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":["unknown"]},{"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":8,"references":[],"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":"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":9,"references":[2,8],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"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":["unknown"]},{"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":11,"references":[0,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":"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":["unknown"]},{"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":["unknown"]},{"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":12,"references":[1,11],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":13,"references":[11,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":["unknown"]},{"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":["unknown"]},{"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":14,"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":["unknown"]},{"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":15,"references":[1,14],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":16,"references":[0,1],"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":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.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":"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":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","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":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"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":[]}],"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_in_seconds","getter_name":"durationInSeconds","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":["unknown"]},{"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":"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"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"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_in_seconds","getter_name":"durationInSeconds","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"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":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":7,"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":["unknown"]},{"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":["unknown"]},{"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":8,"references":[],"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":"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":9,"references":[2,8],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"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":["unknown"]},{"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":11,"references":[0,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":"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":["unknown"]},{"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":["unknown"]},{"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":12,"references":[1,11],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":13,"references":[11,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":["unknown"]},{"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":["unknown"]},{"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":14,"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":["unknown"]},{"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":15,"references":[1,14],"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":["unknown"]},{"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":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":16,"references":[0,1],"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":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index 89eb092a16..e629604d6a 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -138,6 +138,7 @@ struct PlatformAsset: Hashable { var width: Int64? = nil var height: Int64? = nil var durationInSeconds: Int64 + var orientation: Int64 // swift-format-ignore: AlwaysUseLowerCamelCase @@ -150,6 +151,7 @@ struct PlatformAsset: Hashable { let width: Int64? = nilOrValue(pigeonVar_list[5]) let height: Int64? = nilOrValue(pigeonVar_list[6]) let durationInSeconds = pigeonVar_list[7] as! Int64 + let orientation = pigeonVar_list[8] as! Int64 return PlatformAsset( id: id, @@ -159,7 +161,8 @@ struct PlatformAsset: Hashable { updatedAt: updatedAt, width: width, height: height, - durationInSeconds: durationInSeconds + durationInSeconds: durationInSeconds, + orientation: orientation ) } func toList() -> [Any?] { @@ -172,6 +175,7 @@ struct PlatformAsset: Hashable { width, height, durationInSeconds, + orientation, ] } static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index 85f3b1fcfb..459e29fa5a 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -27,7 +27,8 @@ extension PHAsset { updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, width: Int64(pixelWidth), height: Int64(pixelHeight), - durationInSeconds: Int64(duration) + durationInSeconds: Int64(duration), + orientation: 0 ) } } @@ -169,7 +170,8 @@ class NativeSyncApiImpl: NativeSyncApi { id: asset.localIdentifier, name: "", type: 0, - durationInSeconds: 0 + durationInSeconds: 0, + orientation: 0 ) if (updatedAssets.contains(AssetWrapper(with: predicate))) { continue diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 1226d1730f..fb67b298c1 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -25,6 +25,7 @@ sealed class BaseAsset { final int? height; final int? durationInSeconds; final bool isFavorite; + final String? livePhotoVideoId; const BaseAsset({ required this.name, @@ -36,18 +37,12 @@ sealed class BaseAsset { this.height, this.durationInSeconds, this.isFavorite = false, + this.livePhotoVideoId, }); bool get isImage => type == AssetType.image; bool get isVideo => type == AssetType.video; - double? get aspectRatio { - if (width != null && height != null && height! > 0) { - return width! / height!; - } - return null; - } - bool get hasRemote => storage == AssetState.remote || storage == AssetState.merged; bool get hasLocal => diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 30a4955fa8..779ca28247 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -3,6 +3,7 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { final String id; final String? remoteId; + final int orientation; const LocalAsset({ required this.id, @@ -16,6 +17,8 @@ class LocalAsset extends BaseAsset { super.height, super.durationInSeconds, super.isFavorite = false, + super.livePhotoVideoId, + this.orientation = 0, }); @override @@ -38,6 +41,7 @@ class LocalAsset extends BaseAsset { durationInSeconds: ${durationInSeconds ?? ""}, remoteId: ${remoteId ?? ""} isFavorite: $isFavorite, + orientation: $orientation, }'''; } @@ -45,11 +49,15 @@ class LocalAsset extends BaseAsset { bool operator ==(Object other) { if (other is! LocalAsset) return false; if (identical(this, other)) return true; - return super == other && id == other.id && remoteId == other.remoteId; + return super == other && + id == other.id && + remoteId == other.remoteId && + orientation == other.orientation; } @override - int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; + int get hashCode => + super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode; LocalAsset copyWith({ String? id, @@ -63,6 +71,7 @@ class LocalAsset extends BaseAsset { int? height, int? durationInSeconds, bool? isFavorite, + int? orientation, }) { return LocalAsset( id: id ?? this.id, @@ -76,6 +85,7 @@ class LocalAsset extends BaseAsset { height: height ?? this.height, durationInSeconds: durationInSeconds ?? this.durationInSeconds, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); } } diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index d04f3340ac..1afd64a34b 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -30,6 +30,7 @@ class RemoteAsset extends BaseAsset { super.isFavorite = false, this.thumbHash, this.visibility = AssetVisibility.timeline, + super.livePhotoVideoId, }); @override diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index a256ee3589..99246c31a1 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -5,6 +5,7 @@ enum Setting { groupAssetsBy(StoreKey.groupAssetsBy, 0), showStorageIndicator(StoreKey.storageIndicator, true), loadOriginal(StoreKey.loadOriginal, false), + loadOriginalVideo(StoreKey.loadOriginalVideo, false), preferRemoteImage(StoreKey.preferRemoteImage, false), advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), ; diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index c4a0766601..197a6d5801 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -2,16 +2,20 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.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/utils/exif.converter.dart'; +import 'package:platform/platform.dart'; class AssetService { final RemoteAssetRepository _remoteAssetRepository; final DriftLocalAssetRepository _localAssetRepository; + final Platform _platform; const AssetService({ required RemoteAssetRepository remoteAssetRepository, required DriftLocalAssetRepository localAssetRepository, }) : _remoteAssetRepository = remoteAssetRepository, - _localAssetRepository = localAssetRepository; + _localAssetRepository = localAssetRepository, + _platform = const LocalPlatform(); Stream watchAsset(BaseAsset asset) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; @@ -21,10 +25,39 @@ class AssetService { } Future getExif(BaseAsset asset) async { - if (asset is LocalAsset || asset is! RemoteAsset) { + if (!asset.hasRemote) { return null; } - return _remoteAssetRepository.getExif(asset.id); + final id = + asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id; + return _remoteAssetRepository.getExif(id); + } + + Future getAspectRatio(BaseAsset asset) async { + bool isFlipped; + double? width; + double? height; + + if (asset.hasRemote) { + final exif = await getExif(asset); + isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation); + width = exif?.width ?? asset.width?.toDouble(); + height = exif?.height ?? asset.height?.toDouble(); + } else if (asset is LocalAsset && _platform.isAndroid) { + isFlipped = asset.orientation == 90 || asset.orientation == 270; + width = asset.height?.toDouble(); + height = asset.width?.toDouble(); + } else { + isFlipped = false; + } + + final orientedWidth = isFlipped ? height : width; + final orientedHeight = isFlipped ? width : height; + if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { + return orientedWidth / orientedHeight; + } + + return 1.0; } } diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index 87579067c8..d004041276 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -61,7 +61,7 @@ class HashService { final toHash = <_AssetToPath>[]; for (final asset in assetsToHash) { - final file = await _storageRepository.getFileForAsset(asset); + final file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { continue; } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 7a61dbc0c7..8d3b747b37 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -359,6 +359,7 @@ extension on Iterable { width: e.width, height: e.height, durationInSeconds: e.durationInSeconds, + orientation: e.orientation, ), ).toList(); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 62f91ae458..204d5d6a80 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -14,6 +14,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { // Only used during backup to mirror the favorite status of the asset in the server BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + IntColumn get orientation => integer().withDefault(const Constant(0))(); + @override Set get primaryKey => {id}; } @@ -31,5 +33,6 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { height: height, width: width, remoteId: null, + orientation: orientation, ); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index a3c79b2e2e..e9c5961aa5 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -20,6 +20,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder required String id, i0.Value checksum, i0.Value isFavorite, + i0.Value orientation, }); typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i1.LocalAssetEntityCompanion Function({ @@ -33,6 +34,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder i0.Value id, i0.Value checksum, i0.Value isFavorite, + i0.Value orientation, }); class $$LocalAssetEntityTableFilterComposer @@ -76,6 +78,10 @@ class $$LocalAssetEntityTableFilterComposer i0.ColumnFilters get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnFilters(column)); } class $$LocalAssetEntityTableOrderingComposer @@ -120,6 +126,10 @@ class $$LocalAssetEntityTableOrderingComposer i0.ColumnOrderings get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnOrderings(column)); } class $$LocalAssetEntityTableAnnotationComposer @@ -160,6 +170,9 @@ class $$LocalAssetEntityTableAnnotationComposer i0.GeneratedColumn get isFavorite => $composableBuilder( column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get orientation => $composableBuilder( + column: $table.orientation, builder: (column) => column); } class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< @@ -201,6 +214,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion( name: name, @@ -213,6 +227,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< id: id, checksum: checksum, isFavorite: isFavorite, + orientation: orientation, ), createCompanionCallback: ({ required String name, @@ -225,6 +240,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< required String id, i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion.insert( name: name, @@ -237,6 +253,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< id: id, checksum: checksum, isFavorite: isFavorite, + orientation: orientation, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -337,6 +354,14 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("is_favorite" IN (0, 1))'), defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _orientationMeta = + const i0.VerificationMeta('orientation'); + @override + late final i0.GeneratedColumn orientation = i0.GeneratedColumn( + 'orientation', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0)); @override List get $columns => [ name, @@ -348,7 +373,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity durationInSeconds, id, checksum, - isFavorite + isFavorite, + orientation ]; @override String get aliasedName => _alias ?? actualTableName; @@ -404,6 +430,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity isFavorite.isAcceptableOrUnknown( data['is_favorite']!, _isFavoriteMeta)); } + if (data.containsKey('orientation')) { + context.handle( + _orientationMeta, + orientation.isAcceptableOrUnknown( + data['orientation']!, _orientationMeta)); + } return context; } @@ -435,6 +467,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), isFavorite: attachedDatabase.typeMapping .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + orientation: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}orientation'])!, ); } @@ -463,6 +497,7 @@ class LocalAssetEntityData extends i0.DataClass final String id; final String? checksum; final bool isFavorite; + final int orientation; const LocalAssetEntityData( {required this.name, required this.type, @@ -473,7 +508,8 @@ class LocalAssetEntityData extends i0.DataClass this.durationInSeconds, required this.id, this.checksum, - required this.isFavorite}); + required this.isFavorite, + required this.orientation}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -498,6 +534,7 @@ class LocalAssetEntityData extends i0.DataClass map['checksum'] = i0.Variable(checksum); } map['is_favorite'] = i0.Variable(isFavorite); + map['orientation'] = i0.Variable(orientation); return map; } @@ -516,6 +553,7 @@ class LocalAssetEntityData extends i0.DataClass id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), ); } @override @@ -533,6 +571,7 @@ class LocalAssetEntityData extends i0.DataClass 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), }; } @@ -546,7 +585,8 @@ class LocalAssetEntityData extends i0.DataClass i0.Value durationInSeconds = const i0.Value.absent(), String? id, i0.Value checksum = const i0.Value.absent(), - bool? isFavorite}) => + bool? isFavorite, + int? orientation}) => i1.LocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -560,6 +600,7 @@ class LocalAssetEntityData extends i0.DataClass id: id ?? this.id, checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { return LocalAssetEntityData( @@ -576,6 +617,8 @@ class LocalAssetEntityData extends i0.DataClass checksum: data.checksum.present ? data.checksum.value : this.checksum, isFavorite: data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, ); } @@ -591,14 +634,15 @@ class LocalAssetEntityData extends i0.DataClass ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') - ..write('isFavorite: $isFavorite') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') ..write(')')) .toString(); } @override int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, - height, durationInSeconds, id, checksum, isFavorite); + height, durationInSeconds, id, checksum, isFavorite, orientation); @override bool operator ==(Object other) => identical(this, other) || @@ -612,7 +656,8 @@ class LocalAssetEntityData extends i0.DataClass other.durationInSeconds == this.durationInSeconds && other.id == this.id && other.checksum == this.checksum && - other.isFavorite == this.isFavorite); + other.isFavorite == this.isFavorite && + other.orientation == this.orientation); } class LocalAssetEntityCompanion @@ -627,6 +672,7 @@ class LocalAssetEntityCompanion final i0.Value id; final i0.Value checksum; final i0.Value isFavorite; + final i0.Value orientation; const LocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -638,6 +684,7 @@ class LocalAssetEntityCompanion this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), }); LocalAssetEntityCompanion.insert({ required String name, @@ -650,6 +697,7 @@ class LocalAssetEntityCompanion required String id, this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id); @@ -664,6 +712,7 @@ class LocalAssetEntityCompanion i0.Expression? id, i0.Expression? checksum, i0.Expression? isFavorite, + i0.Expression? orientation, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -676,6 +725,7 @@ class LocalAssetEntityCompanion if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, }); } @@ -689,7 +739,8 @@ class LocalAssetEntityCompanion i0.Value? durationInSeconds, i0.Value? id, i0.Value? checksum, - i0.Value? isFavorite}) { + i0.Value? isFavorite, + i0.Value? orientation}) { return i1.LocalAssetEntityCompanion( name: name ?? this.name, type: type ?? this.type, @@ -701,6 +752,7 @@ class LocalAssetEntityCompanion id: id ?? this.id, checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, ); } @@ -738,6 +790,9 @@ class LocalAssetEntityCompanion if (isFavorite.present) { map['is_favorite'] = i0.Variable(isFavorite.value); } + if (orientation.present) { + map['orientation'] = i0.Variable(orientation.value); + } return map; } @@ -753,7 +808,8 @@ class LocalAssetEntityCompanion ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') - ..write('isFavorite: $isFavorite') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index 51e5e4d73c..d6451db8d8 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -16,7 +16,8 @@ mergedAsset: SELECT * FROM rae.is_favorite, rae.thumb_hash, rae.checksum, - rae.owner_id + rae.owner_id, + 0 as orientation FROM remote_asset_entity rae LEFT JOIN @@ -37,7 +38,8 @@ mergedAsset: SELECT * FROM lae.is_favorite, NULL as thumb_hash, lae.checksum, - NULL as owner_id + NULL as owner_id, + lae.orientation FROM local_asset_entity lae LEFT JOIN diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart index f836dabe6a..b289083a2b 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart +++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart @@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor { final generatedlimit = $write(limit, startIndex: $arrayStartIndex); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}', + 'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, 0 AS orientation FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, lae.orientation FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}', variables: [ for (var $ in var1) i0.Variable($), ...generatedlimit.introducedVariables @@ -42,6 +42,7 @@ class MergedAssetDrift extends i1.ModularAccessor { thumbHash: row.readNullable('thumb_hash'), checksum: row.readNullable('checksum'), ownerId: row.readNullable('owner_id'), + orientation: row.read('orientation'), )); } @@ -87,6 +88,7 @@ class MergedAssetResult { final String? thumbHash; final String? checksum; final String? ownerId; + final int orientation; MergedAssetResult({ this.remoteId, this.localId, @@ -101,6 +103,7 @@ class MergedAssetResult { this.thumbHash, this.checksum, this.ownerId, + required this.orientation, }); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 154e79149a..44ebe7f7ca 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -281,6 +281,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { height: Value(asset.height), durationInSeconds: Value(asset.durationInSeconds), id: asset.id, + orientation: Value(asset.orientation), checksum: const Value(null), ); batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 1f41e48522..5b511709cd 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -1,29 +1,22 @@ import 'dart:io'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; class StorageRepository { const StorageRepository(); - Future getFileForAsset(LocalAsset asset) async { + Future getFileForAsset(String assetId) async { final log = Logger('StorageRepository'); File? file; try { - final entity = await AssetEntity.fromId(asset.id); + final entity = await AssetEntity.fromId(assetId); file = await entity?.originFile; if (file == null) { - log.warning( - "Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", - ); + log.warning("Cannot get file for asset $assetId"); } } catch (error, stackTrace) { - log.warning( - "Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", - error, - stackTrace, - ); + log.warning("Error getting file for asset $assetId", error, stackTrace); } return file; } diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index f67a154398..569df0aed8 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -99,6 +99,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { height: row.height, isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, + orientation: row.orientation, ); }, ).get(); diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index dd6e545f88..46599eb016 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -40,6 +40,7 @@ class PlatformAsset { this.width, this.height, required this.durationInSeconds, + required this.orientation, }); String id; @@ -58,6 +59,8 @@ class PlatformAsset { int durationInSeconds; + int orientation; + List _toList() { return [ id, @@ -68,6 +71,7 @@ class PlatformAsset { width, height, durationInSeconds, + orientation, ]; } @@ -86,6 +90,7 @@ class PlatformAsset { width: result[5] as int?, height: result[6] as int?, durationInSeconds: result[7]! as int, + orientation: result[8]! as int, ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index c91b71319c..aea821d945 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -11,8 +12,11 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.sta import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -78,6 +82,7 @@ class _AssetViewerState extends ConsumerState { Offset dragDownPosition = Offset.zero; int totalAssets = 0; BuildContext? scaffoldContext; + Map videoPlayerKeys = {}; // Delayed operations that should be cancelled on disposal final List _delayedOperations = []; @@ -158,6 +163,11 @@ class _AssetViewerState extends ConsumerState { void _onAssetChanged(int index) { final asset = ref.read(timelineServiceProvider).getAsset(index); ref.read(currentAssetNotifier.notifier).setAsset(asset); + if (asset.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + unawaited(ref.read(timelineServiceProvider).preCacheAssets(index)); _cancelTimers(); // This will trigger the pre-caching of adjacent assets ensuring @@ -455,11 +465,25 @@ class _AssetViewerState extends ConsumerState { ); } + void _onScaleStateChanged(PhotoViewScaleState scaleState) { + if (scaleState != PhotoViewScaleState.initial) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + } + PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) { scaffoldContext ??= ctx; final asset = ref.read(timelineServiceProvider).getAsset(index); - final size = Size(ctx.width, ctx.height); + if (asset.isImage) { + return _imageBuilder(ctx, asset); + } + + return _videoBuilder(ctx, asset); + } + + PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) { + final size = Size(ctx.width, ctx.height); return PhotoViewGalleryPageOptions( key: ValueKey(asset.heroTag), imageProvider: getFullImageProvider(asset, size: size), @@ -486,6 +510,43 @@ class _AssetViewerState extends ConsumerState { ); } + GlobalKey _getVideoPlayerKey(String id) { + videoPlayerKeys.putIfAbsent(id, () => GlobalKey()); + return videoPlayerKeys[id]!; + } + + PhotoViewGalleryPageOptions _videoBuilder(BuildContext ctx, BaseAsset asset) { + return PhotoViewGalleryPageOptions.customChild( + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onTapDown: _onTapDown, + heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag), + filterQuality: FilterQuality.high, + initialScale: PhotoViewComputedScale.contained * 0.99, + maxScale: 1.0, + minScale: PhotoViewComputedScale.contained * 0.99, + basePosition: Alignment.center, + child: SizedBox( + width: ctx.width, + height: ctx.height, + child: NativeVideoViewer( + key: _getVideoPlayerKey(asset.heroTag), + asset: asset, + image: Image( + key: ValueKey(asset), + image: + getFullImageProvider(asset, size: Size(ctx.width, ctx.height)), + fit: BoxFit.contain, + height: ctx.height, + width: ctx.width, + alignment: Alignment.center, + ), + ), + ), + ); + } + void _onPop(bool didPop, T? result) { ref.read(currentAssetNotifier.notifier).dispose(); } @@ -518,6 +579,7 @@ class _AssetViewerState extends ConsumerState { itemCount: totalAssets, onPageChanged: _onPageChanged, onPageBuild: _onPageBuild, + scaleStateChangedCallback: _onScaleStateChanged, builder: _assetBuilder, backgroundDecoration: BoxDecoration(color: backgroundColor), enablePanAlways: true, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart index 231d40c9ca..020d1d9b2c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; class AssetViewerState { @@ -63,6 +64,13 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier { showingBottomSheet: showing, showingControls: showing ? true : state.showingControls, ); + if (showing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } + } + + void setControls(bool isShowing) { + state = state.copyWith(showingControls: isShowing); } void toggleControls() { diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 6269bef6be..a28d5eafa4 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; class ViewerBottomBar extends ConsumerWidget { const ViewerBottomBar({super.key}); @@ -65,11 +66,17 @@ class ViewerBottomBar extends ConsumerWidget { ), ), child: Container( - height: 80, + height: asset.isVideo ? 160 : 80, color: Colors.black.withAlpha(125), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: actions, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (asset.isVideo) const VideoControls(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: actions, + ), + ], ), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart new file mode 100644 index 0000000000..60cb61b255 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -0,0 +1,429 @@ +import 'dart:async'; +import 'dart:io'; + +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/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/debounce.dart'; +import 'package:immich_mobile/utils/hooks/interval_hook.dart'; +import 'package:logging/logging.dart'; +import 'package:native_video_player/native_video_player.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +class NativeVideoViewer extends HookConsumerWidget { + final BaseAsset asset; + final bool showControls; + final int playbackDelayFactor; + final Widget image; + + const NativeVideoViewer({ + super.key, + required this.asset, + required this.image, + this.showControls = true, + this.playbackDelayFactor = 1, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useState(null); + final lastVideoPosition = useRef(-1); + final isBuffering = useRef(false); + + // Used to track whether the video should play when the app + // is brought back to the foreground + final shouldPlayOnForeground = useRef(true); + + // When a video is opened through the timeline, `isCurrent` will immediately be true. + // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. + // If the swipe is completed, `isCurrent` will be true for video B after a delay. + // If the swipe is canceled, `currentAsset` will not have changed and video A will continue to play. + final currentAsset = useState(ref.read(currentAssetNotifier)); + final isCurrent = currentAsset.value == asset; + + // Used to show the placeholder during hero animations for remote videos to avoid a stutter + final isVisible = useState(Platform.isIOS && asset.hasLocal); + + final log = Logger('NativeVideoViewerPage'); + + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + + Future createSource() async { + if (!context.mounted) { + return null; + } + + try { + if (asset.hasLocal && asset.livePhotoVideoId == null) { + final id = asset is LocalAsset + ? (asset as LocalAsset).id + : (asset as RemoteAsset).localId!; + final file = await const StorageRepository().getFileForAsset(id); + if (file == null) { + throw Exception('No file found for the video'); + } + + final source = await VideoSource.init( + path: file.path, + type: VideoSourceType.file, + ); + return source; + } + + final remoteId = (asset as RemoteAsset).id; + + // Use a network URL for the video player controller + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final isOriginalVideo = + ref.read(settingsProvider).get(Setting.loadOriginalVideo); + final String postfixUrl = + isOriginalVideo ? 'original' : 'video/playback'; + final String videoUrl = asset.livePhotoVideoId != null + ? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl' + : '$serverEndpoint/assets/$remoteId/$postfixUrl'; + + final source = await VideoSource.init( + path: videoUrl, + type: VideoSourceType.network, + headers: ApiService.getRequestHeaders(), + ); + return source; + } catch (error) { + log.severe( + 'Error creating video source for asset ${asset.name}: $error', + ); + return null; + } + } + + final videoSource = useMemoized>(() => createSource()); + final aspectRatio = useState(null); + useMemoized( + () async { + if (!context.mounted || aspectRatio.value != null) { + return null; + } + + try { + aspectRatio.value = + await ref.read(assetServiceProvider).getAspectRatio(asset); + } catch (error) { + log.severe( + 'Error getting aspect ratio for asset ${asset.name}: $error', + ); + } + }, + [asset.heroTag], + ); + + void checkIfBuffering() { + if (!context.mounted) { + return; + } + + final videoPlayback = ref.read(videoPlaybackValueProvider); + if ((isBuffering.value || + videoPlayback.state == VideoPlaybackState.initializing) && + videoPlayback.state != VideoPlaybackState.buffering) { + ref.read(videoPlaybackValueProvider.notifier).value = + videoPlayback.copyWith(state: VideoPlaybackState.buffering); + } + } + + // Timer to mark videos as buffering if the position does not change + useInterval(const Duration(seconds: 5), checkIfBuffering); + + // When the position changes, seek to the position + // Debounce the seek to avoid seeking too often + // But also don't delay the seek too much to maintain visual feedback + final seekDebouncer = useDebouncer( + interval: const Duration(milliseconds: 100), + maxWaitTime: const Duration(milliseconds: 200), + ); + ref.listen(videoPlayerControlsProvider, (oldControls, newControls) async { + final playerController = controller.value; + if (playerController == null) { + return; + } + + final playbackInfo = playerController.playbackInfo; + if (playbackInfo == null) { + return; + } + + final oldSeek = (oldControls?.position ?? 0) ~/ 1; + final newSeek = newControls.position ~/ 1; + if (oldSeek != newSeek || newControls.restarted) { + seekDebouncer.run(() => playerController.seekTo(newSeek)); + } + + if (oldControls?.pause != newControls.pause || newControls.restarted) { + // Make sure the last seek is complete before pausing or playing + // Otherwise, `onPlaybackPositionChanged` can receive outdated events + if (seekDebouncer.isActive) { + await seekDebouncer.drain(); + } + + try { + if (newControls.pause) { + await playerController.pause(); + } else { + await playerController.play(); + } + } catch (error) { + log.severe('Error pausing or playing video: $error'); + } + } + }); + + void onPlaybackReady() async { + final videoController = controller.value; + if (videoController == null || !isCurrent || !context.mounted) { + return; + } + + final videoPlayback = + VideoPlaybackValue.fromNativeController(videoController); + ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + + if (ref.read(assetViewerProvider.select((s) => s.showingBottomSheet))) { + return; + } + + try { + await videoController.play(); + await videoController.setVolume(0.9); + } catch (error) { + log.severe('Error playing video: $error'); + } + } + + void onPlaybackStatusChanged() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final videoPlayback = + VideoPlaybackValue.fromNativeController(videoController); + if (videoPlayback.state == VideoPlaybackState.playing) { + // Sync with the controls playing + WakelockPlus.enable(); + } else { + // Sync with the controls pause + WakelockPlus.disable(); + } + + ref.read(videoPlaybackValueProvider.notifier).status = + videoPlayback.state; + } + + void onPlaybackPositionChanged() { + // When seeking, these events sometimes move the slider to an older position + if (seekDebouncer.isActive) { + return; + } + + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + final playbackInfo = videoController.playbackInfo; + if (playbackInfo == null) { + return; + } + + ref.read(videoPlaybackValueProvider.notifier).position = + Duration(seconds: playbackInfo.position); + + // Check if the video is buffering + if (playbackInfo.status == PlaybackStatus.playing) { + isBuffering.value = lastVideoPosition.value == playbackInfo.position; + lastVideoPosition.value = playbackInfo.position; + } else { + isBuffering.value = false; + lastVideoPosition.value = -1; + } + } + + void onPlaybackEnded() { + final videoController = controller.value; + if (videoController == null || !context.mounted) { + return; + } + + if (videoController.playbackInfo?.status == PlaybackStatus.stopped && + !ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.loopVideo)) { + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; + } + } + + void removeListeners(NativeVideoPlayerController controller) { + controller.onPlaybackPositionChanged + .removeListener(onPlaybackPositionChanged); + controller.onPlaybackStatusChanged + .removeListener(onPlaybackStatusChanged); + controller.onPlaybackReady.removeListener(onPlaybackReady); + controller.onPlaybackEnded.removeListener(onPlaybackEnded); + } + + void initController(NativeVideoPlayerController nc) async { + if (controller.value != null || !context.mounted) { + return; + } + ref.read(videoPlayerControlsProvider.notifier).reset(); + ref.read(videoPlaybackValueProvider.notifier).reset(); + + final source = await videoSource; + if (source == null) { + return; + } + + nc.onPlaybackPositionChanged.addListener(onPlaybackPositionChanged); + nc.onPlaybackStatusChanged.addListener(onPlaybackStatusChanged); + nc.onPlaybackReady.addListener(onPlaybackReady); + nc.onPlaybackEnded.addListener(onPlaybackEnded); + + nc.loadVideoSource(source).catchError((error) { + log.severe('Error loading video source: $error'); + }); + final loopVideo = ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.loopVideo); + nc.setLoop(loopVideo); + + controller.value = nc; + Timer(const Duration(milliseconds: 200), checkIfBuffering); + } + + ref.listen(currentAssetNotifier, (_, value) { + final playerController = controller.value; + if (playerController != null && value != asset) { + removeListeners(playerController); + } + + final curAsset = currentAsset.value; + if (curAsset == asset) { + return; + } + + final imageToVideo = curAsset != null && !curAsset.isVideo; + + // No need to delay video playback when swiping from an image to a video + if (imageToVideo && Platform.isIOS) { + currentAsset.value = value; + onPlaybackReady(); + return; + } + + // Delay the video playback to avoid a stutter in the swipe animation + // Note, in some circumstances a longer delay is needed (eg: memories), + // the playbackDelayFactor can be used for this + // This delay seems like a hacky way to resolve underlying bugs in video + // playback, but other resolutions failed thus far + Timer( + Platform.isIOS + ? Duration(milliseconds: 300 * playbackDelayFactor) + : imageToVideo + ? Duration(milliseconds: 200 * playbackDelayFactor) + : Duration(milliseconds: 400 * playbackDelayFactor), () { + if (!context.mounted) { + return; + } + + currentAsset.value = value; + if (currentAsset.value == asset) { + onPlaybackReady(); + } + }); + }); + + useEffect( + () { + // If opening a remote video from a hero animation, delay visibility to avoid a stutter + final timer = isVisible.value + ? null + : Timer( + const Duration(milliseconds: 300), + () => isVisible.value = true, + ); + + return () { + timer?.cancel(); + final playerController = controller.value; + if (playerController == null) { + return; + } + removeListeners(playerController); + playerController.stop().catchError((error) { + log.fine('Error stopping video: $error'); + }); + + WakelockPlus.disable(); + }; + }, + const [], + ); + + useOnAppLifecycleStateChange((_, state) async { + if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { + controller.value?.play(); + } else if (state == AppLifecycleState.paused) { + final videoPlaying = await controller.value?.isPlaying(); + if (videoPlaying ?? true) { + shouldPlayOnForeground.value = true; + controller.value?.pause(); + } else { + shouldPlayOnForeground.value = false; + } + } + }); + + return Stack( + children: [ + // This remains under the video to avoid flickering + // For motion videos, this is the image portion of the asset + Center(key: ValueKey(asset.heroTag), child: image), + if (aspectRatio.value != null && !isCasting) + Visibility.maintain( + key: ValueKey(asset), + visible: isVisible.value, + child: Center( + key: ValueKey(asset), + child: AspectRatio( + key: ValueKey(asset), + aspectRatio: aspectRatio.value!, + child: isCurrent + ? NativeVideoPlayerView( + key: ValueKey(asset), + onViewReady: initController, + ) + : null, + ), + ), + ), + if (showControls) const Center(child: VideoViewerControls()), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart new file mode 100644 index 0000000000..883169ae83 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/utils/hooks/timer_hook.dart'; +import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; +import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; + +class VideoViewerControls extends HookConsumerWidget { + final Duration hideTimerDuration; + + const VideoViewerControls({ + super.key, + this.hideTimerDuration = const Duration(seconds: 5), + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetIsVideo = ref.watch( + currentAssetNotifier.select((asset) => asset != null && asset.isVideo), + ); + bool showControls = + ref.watch(assetViewerProvider.select((s) => s.showingControls)); + final showBottomSheet = + ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); + if (showBottomSheet) { + showControls = false; + } + final VideoPlaybackState state = + ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + + final cast = ref.watch(castProvider); + + // A timer to hide the controls + final hideTimer = useTimer( + hideTimerDuration, + () { + if (!context.mounted) { + return; + } + final state = ref.read(videoPlaybackValueProvider).state; + + // Do not hide on paused + if (state != VideoPlaybackState.paused && + state != VideoPlaybackState.completed && + assetIsVideo) { + ref.read(assetViewerProvider.notifier).setControls(false); + } + }, + ); + final showBuffering = + state == VideoPlaybackState.buffering && !cast.isCasting; + + /// Shows the controls and starts the timer to hide them + void showControlsAndStartHideTimer() { + hideTimer.reset(); + ref.read(assetViewerProvider.notifier).setControls(true); + } + + // When we change position, show or hide timer + ref.listen(videoPlayerControlsProvider.select((v) => v.position), + (previous, next) { + showControlsAndStartHideTimer(); + }); + + /// Toggles between playing and pausing depending on the state of the video + void togglePlay() { + showControlsAndStartHideTimer(); + + if (cast.isCasting) { + if (cast.castState == CastState.playing) { + ref.read(castProvider.notifier).pause(); + } else if (cast.castState == CastState.paused) { + ref.read(castProvider.notifier).play(); + } else if (cast.castState == CastState.idle) { + // resend the play command since its finished + final asset = ref.read(currentAssetNotifier); + if (asset == null) { + return; + } + // ref.read(castProvider.notifier).loadMedia(asset, true); + } + return; + } + + if (state == VideoPlaybackState.playing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } else if (state == VideoPlaybackState.completed) { + ref.read(videoPlayerControlsProvider.notifier).restart(); + } else { + ref.read(videoPlayerControlsProvider.notifier).play(); + } + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: showControlsAndStartHideTimer, + child: AbsorbPointer( + absorbing: !showControls, + child: Stack( + children: [ + if (showBuffering) + const Center( + child: DelayedLoadingIndicator( + fadeInDuration: Duration(milliseconds: 400), + ), + ) + else + GestureDetector( + onTap: () => + ref.read(assetViewerProvider.notifier).setControls(false), + child: CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: state == VideoPlaybackState.completed, + isPlaying: state == VideoPlaybackState.playing || + (cast.isCasting && cast.castState == CastState.playing), + show: assetIsVideo && showControls, + onPressed: togglePlay, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 214dede1af..f046fcad47 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -175,7 +175,7 @@ class LocalFullImageProvider extends ImageProvider { LocalFullImageProvider key, ImageDecoderCallback decode, ) async* { - final file = await _storageRepository.getFileForAsset(key.asset); + final file = await _storageRepository.getFileForAsset(key.asset.id); if (file == null) { throw StateError("Opening file for asset ${key.asset.name} failed"); } diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 8268196089..a6262bd2e5 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; @@ -67,26 +68,20 @@ class DriftMemoryCard extends StatelessWidget { } else { return Hero( tag: 'memory-${asset.id}', - // child: SizedBox( - // width: context.width, - // height: context.height, - // child: NativeVideoViewerPage( - // key: ValueKey(asset.id), - // asset: asset, - // showControls: false, - // playbackDelayFactor: 2, - // image: ImmichImage( - // asset, - // width: context.width, - // height: context.height, - // fit: BoxFit.contain, - // ), - // ), - // ), - child: FullImage( - asset, - fit: fit, - size: const Size(double.infinity, double.infinity), + child: SizedBox( + width: context.width, + height: context.height, + child: NativeVideoViewer( + key: ValueKey(asset.id), + asset: asset, + showControls: false, + playbackDelayFactor: 2, + image: FullImage( + asset, + size: Size(context.width, context.height), + fit: BoxFit.contain, + ), + ), ), ); } diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index 33e2429405..4f14b7a0b9 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -23,6 +23,7 @@ class PlatformAsset { final int? width; final int? height; final int durationInSeconds; + final int orientation; const PlatformAsset({ required this.id, @@ -33,6 +34,7 @@ class PlatformAsset { this.width, this.height, this.durationInSeconds = 0, + this.orientation = 0, }); } diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 2a7ea740bf..b2ab803ac2 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -68,7 +68,7 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) + when(() => mockStorageRepo.getFileForAsset(asset.id)) .thenAnswer((_) async => null); await sut.hashAssets(); @@ -89,7 +89,7 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) + when(() => mockStorageRepo.getFileForAsset(asset.id)) .thenAnswer((_) async => mockFile); when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer( (_) async => [hash], @@ -116,7 +116,7 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) + when(() => mockStorageRepo.getFileForAsset(asset.id)) .thenAnswer((_) async => mockFile); when(() => mockNativeApi.hashPaths(['image-path'])) .thenAnswer((_) async => [null]); @@ -141,7 +141,7 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); - when(() => mockStorageRepo.getFileForAsset(asset)) + when(() => mockStorageRepo.getFileForAsset(asset.id)) .thenAnswer((_) async => mockFile); final invalidHash = Uint8List.fromList([1, 2, 3]); @@ -180,9 +180,9 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) + when(() => mockStorageRepo.getFileForAsset(asset1.id)) .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) + when(() => mockStorageRepo.getFileForAsset(asset2.id)) .thenAnswer((_) async => mockFile2); final hash = Uint8List.fromList(List.generate(20, (i) => i)); @@ -220,9 +220,9 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) + when(() => mockStorageRepo.getFileForAsset(asset1.id)) .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) + when(() => mockStorageRepo.getFileForAsset(asset2.id)) .thenAnswer((_) async => mockFile2); final hash = Uint8List.fromList(List.generate(20, (i) => i)); @@ -252,9 +252,9 @@ void main() { .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); - when(() => mockStorageRepo.getFileForAsset(asset1)) + when(() => mockStorageRepo.getFileForAsset(asset1.id)) .thenAnswer((_) async => mockFile1); - when(() => mockStorageRepo.getFileForAsset(asset2)) + when(() => mockStorageRepo.getFileForAsset(asset2.id)) .thenAnswer((_) async => mockFile2); final validHash = Uint8List.fromList(List.generate(20, (i) => i));