mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge 662c2b8749 into ffc83eae36
This commit is contained in:
commit
158cd5b268
15 changed files with 4553 additions and 11 deletions
|
|
@ -82,6 +82,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||
add(MediaStore.MediaColumns.HEIGHT)
|
||||
add(MediaStore.MediaColumns.DURATION)
|
||||
add(MediaStore.MediaColumns.ORIENTATION)
|
||||
add(MediaStore.MediaColumns.SIZE)
|
||||
// IS_FAVORITE is only available on Android 11 and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
|
|
@ -149,6 +150,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||
val orientationColumn =
|
||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||
val sizeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
val specialFormatColumn = c.getColumnIndex(SPECIAL_FORMAT_COLUMN)
|
||||
val xmpColumn = c.getColumnIndex(MediaStore.MediaColumns.XMP)
|
||||
|
|
@ -186,6 +188,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
|
||||
else c.getLong(durationColumn)
|
||||
val orientation = c.getInt(orientationColumn)
|
||||
val size = c.getLong(sizeColumn)
|
||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||
|
||||
val playbackStyle = detectPlaybackStyle(
|
||||
|
|
@ -204,6 +207,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
|||
duration,
|
||||
0L,
|
||||
isFavorite,
|
||||
size = size,
|
||||
playbackStyle = playbackStyle,
|
||||
)
|
||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||
|
|
|
|||
3636
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
3636
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ const int kLogTruncateLimit = 2000;
|
|||
// Sync
|
||||
const int kSyncEventBatchSize = 5000;
|
||||
const int kFetchLocalAssetsBatchSize = 40000;
|
||||
final DateTime kLocalAlbumNeverSynced = .utc(1);
|
||||
|
||||
// Hash batch limits
|
||||
final int kBatchHashFileLimit = Platform.isIOS ? 32 : 512;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ class LocalAsset extends BaseAsset {
|
|||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
final int? size;
|
||||
|
||||
const LocalAsset({
|
||||
required this.id,
|
||||
String? remoteId,
|
||||
|
|
@ -31,6 +33,7 @@ class LocalAsset extends BaseAsset {
|
|||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.size,
|
||||
required super.isEdited,
|
||||
}) : remoteAssetId = remoteId;
|
||||
|
||||
|
|
@ -87,7 +90,8 @@ class LocalAsset extends BaseAsset {
|
|||
playbackStyle == other.playbackStyle &&
|
||||
adjustmentTime == other.adjustmentTime &&
|
||||
latitude == other.latitude &&
|
||||
longitude == other.longitude;
|
||||
longitude == other.longitude &&
|
||||
size == other.size;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -99,7 +103,8 @@ class LocalAsset extends BaseAsset {
|
|||
playbackStyle.hashCode ^
|
||||
adjustmentTime.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode;
|
||||
longitude.hashCode ^
|
||||
size.hashCode;
|
||||
|
||||
LocalAsset copyWith({
|
||||
String? id,
|
||||
|
|
@ -119,6 +124,7 @@ class LocalAsset extends BaseAsset {
|
|||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
int? size,
|
||||
bool? isEdited,
|
||||
}) {
|
||||
return LocalAsset(
|
||||
|
|
@ -139,6 +145,7 @@ class LocalAsset extends BaseAsset {
|
|||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
size: size ?? this.size,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ class LocalSyncService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> resetFullSync() async {
|
||||
_log.info("Resetting local sync state, the next sync will re-read every asset");
|
||||
await _nativeSyncApi.clearSyncCheckpoint();
|
||||
await _localAlbumRepository.resetSync();
|
||||
}
|
||||
|
||||
Future<void> fullSync() async {
|
||||
try {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
|
|
@ -306,7 +312,7 @@ class LocalSyncService {
|
|||
both: (dbAsset, deviceAsset) {
|
||||
// Custom comparison to check if the asset has been modified without
|
||||
// comparing the checksum
|
||||
if (!_assetsEqual(dbAsset, deviceAsset)) {
|
||||
if (!assetsEqual(dbAsset, deviceAsset)) {
|
||||
assetsToUpsert.add(deviceAsset);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -360,13 +366,21 @@ class LocalSyncService {
|
|||
// await _localAlbumRepository.updateCloudMapping(cloudMapping);
|
||||
}
|
||||
|
||||
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
||||
@visibleForTesting
|
||||
bool assetsEqual(LocalAsset a, LocalAsset b) {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||
return a.id == b.id &&
|
||||
a.name == b.name &&
|
||||
a.type == b.type &&
|
||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||
a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||
a.width == b.width &&
|
||||
a.height == b.height &&
|
||||
a.durationMs == b.durationMs;
|
||||
a.durationMs == b.durationMs &&
|
||||
a.isFavorite == b.isFavorite &&
|
||||
a.orientation == b.orientation &&
|
||||
a.playbackStyle == b.playbackStyle &&
|
||||
a.size == b.size;
|
||||
}
|
||||
|
||||
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||
|
|
@ -468,6 +482,7 @@ extension PlatformToLocalAsset on PlatformAsset {
|
|||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
size: size,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||
|
||||
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
||||
|
||||
IntColumn get size => integer().nullable()();
|
||||
|
||||
TextColumn get iCloudId => text().nullable()();
|
||||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
|
|
@ -46,6 +48,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
|||
width: width,
|
||||
remoteId: remoteId,
|
||||
orientation: orientation,
|
||||
size: size,
|
||||
playbackStyle: playbackStyle,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class Drift extends $Drift {
|
|||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 31;
|
||||
int get schemaVersion => 32;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
|
|
@ -318,6 +318,9 @@ class Drift extends $Drift {
|
|||
from30To31: (m, v31) async {
|
||||
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||
},
|
||||
from31To32: (m, v32) async {
|
||||
await m.addColumn(v32.localAssetEntity, v32.localAssetEntity.size);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16506,6 +16506,638 @@ final class Schema31 extends i0.VersionedSchema {
|
|||
);
|
||||
}
|
||||
|
||||
final class Schema32 extends i0.VersionedSchema {
|
||||
Schema32({required super.database}) : super(version: 32);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxLocalAssetCreatedAt,
|
||||
idxStackPrimaryAssetId,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||
idxRemoteAssetUploaded,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
settings,
|
||||
assetOcrEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteExifCity,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxAssetFaceVisiblePerson,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
idxAssetEditAssetId,
|
||||
idxAssetOcrAssetId,
|
||||
];
|
||||
late final Shape33 userEntity = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape50 remoteAssetEntity = Shape50(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_119,
|
||||
_column_120,
|
||||
_column_121,
|
||||
_column_122,
|
||||
_column_123,
|
||||
_column_124,
|
||||
_column_212,
|
||||
_column_125,
|
||||
_column_126,
|
||||
_column_127,
|
||||
_column_128,
|
||||
_column_129,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 stackEntity = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_130,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape52 localAssetEntity = Shape52(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_225,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape48 remoteAlbumEntity = Shape48(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_138,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_139,
|
||||
_column_140,
|
||||
_column_141,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 localAlbumEntity = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_115,
|
||||
_column_142,
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_146, _column_147, _column_145],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCreatedAt = i1.Index(
|
||||
'idx_local_asset_created_at',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||
'idx_remote_asset_owner_visibility_deleted_created',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetUploaded = i1.Index(
|
||||
'idx_remote_asset_uploaded',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||
);
|
||||
late final Shape40 authUserEntity = Shape40(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_148,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_149,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_153, _column_154, _column_155],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape41 partnerEntity = Shape41(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_156, _column_157, _column_158],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape42 remoteExifEntity = Shape42(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_117,
|
||||
_column_116,
|
||||
_column_165,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_169,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_176,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_159, _column_177],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_177, _column_153, _column_178],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape44 memoryEntity = Shape44(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_124,
|
||||
_column_121,
|
||||
_column_113,
|
||||
_column_181,
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_159, _column_187],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape45 personEntity = Shape45(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_108,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_192,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape46 assetFaceEntity = Shape46(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_193,
|
||||
_column_194,
|
||||
_column_195,
|
||||
_column_196,
|
||||
_column_197,
|
||||
_column_198,
|
||||
_column_199,
|
||||
_column_200,
|
||||
_column_201,
|
||||
_column_124,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_202, _column_203, _column_204],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_205,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_206,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 assetEditEntity = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_edit_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_207,
|
||||
_column_208,
|
||||
_column_209,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape49 settings = Shape49(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'settings',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY("key")'],
|
||||
columns: [_column_210, _column_224, _column_115],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape51 assetOcrEntity = Shape51(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_ocr_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_213,
|
||||
_column_214,
|
||||
_column_215,
|
||||
_column_216,
|
||||
_column_217,
|
||||
_column_218,
|
||||
_column_219,
|
||||
_column_220,
|
||||
_column_221,
|
||||
_column_222,
|
||||
_column_223,
|
||||
_column_201,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxRemoteExifCity = i1.Index(
|
||||
'idx_remote_exif_city',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||
'idx_asset_face_visible_person',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetOcrAssetId = i1.Index(
|
||||
'idx_asset_ocr_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape52 extends i0.VersionedTable {
|
||||
Shape52({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get width =>
|
||||
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get height =>
|
||||
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get durationMs =>
|
||||
columnsByName['duration_ms']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get checksum =>
|
||||
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isFavorite =>
|
||||
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get orientation =>
|
||||
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get size =>
|
||||
columnsByName['size']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get iCloudId =>
|
||||
columnsByName['i_cloud_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get adjustmentTime =>
|
||||
columnsByName['adjustment_time']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<double> get latitude =>
|
||||
columnsByName['latitude']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get longitude =>
|
||||
columnsByName['longitude']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<int> get playbackStyle =>
|
||||
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_225(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'size',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
|
|
@ -16537,6 +17169,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -16690,6 +17323,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from30To31(migrator, schema);
|
||||
return 31;
|
||||
case 31:
|
||||
final schema = Schema32(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from31To32(migrator, schema);
|
||||
return 32;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -16727,6 +17365,7 @@ i1.OnUpgrade stepByStep({
|
|||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
|
|
@ -16759,5 +17398,6 @@ i1.OnUpgrade stepByStep({
|
|||
from28To29: from28To29,
|
||||
from29To30: from29To30,
|
||||
from30To31: from30To31,
|
||||
from31To32: from31To32,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
|
|
@ -129,6 +130,10 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> resetSync() {
|
||||
return _db.localAlbumEntity.update().write(LocalAlbumEntityCompanion(updatedAt: .new(kLocalAlbumNeverSynced)));
|
||||
}
|
||||
|
||||
Future<void> updateAll(Iterable<LocalAlbum> albums) {
|
||||
return _db.transaction(() async {
|
||||
await _db.localAlbumEntity.update().write(const LocalAlbumEntityCompanion(marker_: Value(true)));
|
||||
|
|
@ -322,6 +327,21 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
await _db.batch((batch) async {
|
||||
for (final asset in localAssets) {
|
||||
batch.update(
|
||||
_db.localAssetEntity,
|
||||
const LocalAssetEntityCompanion(checksum: Value(null)),
|
||||
where: (row) =>
|
||||
row.id.equals(asset.id) &
|
||||
(row.updatedAt.isNotValue(asset.updatedAt) |
|
||||
row.size.isNotExp(Variable(asset.size)) |
|
||||
row.width.isNotExp(Variable(asset.width)) |
|
||||
row.height.isNotExp(Variable(asset.height))),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return _db.batch((batch) async {
|
||||
for (final asset in localAssets) {
|
||||
final companion = LocalAssetEntityCompanion.insert(
|
||||
|
|
@ -333,15 +353,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||
height: Value(asset.height),
|
||||
durationMs: Value(asset.durationMs),
|
||||
id: asset.id,
|
||||
checksum: const Value(null),
|
||||
orientation: Value(asset.orientation),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
playbackStyle: Value(asset.playbackStyle),
|
||||
size: Value(asset.size),
|
||||
);
|
||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||
_db.localAssetEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
||||
companion.copyWith(checksum: const Value(null)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||
]);
|
||||
|
||||
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
properties.insert(5, _PropertyItem(label: 'Size', value: asset.size != null ? '${asset.size} bytes' : null));
|
||||
}
|
||||
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
|
||||
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
|
||||
if (CurrentPlatform.isIOS) {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,16 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/feature_message.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const int targetVersion = 26;
|
||||
const int targetVersion = 27;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int? storedVersion = Store.tryGet(StoreKey.version);
|
||||
|
|
@ -34,6 +36,10 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
|||
await _migrateTo26(drift);
|
||||
}
|
||||
|
||||
if (version < 27 && CurrentPlatform.isAndroid) {
|
||||
await DriftLocalAlbumRepository(drift).resetSync();
|
||||
}
|
||||
|
||||
if (storedVersion == null) {
|
||||
await FeatureMessageService(SettingsRepository.instance).markSeen();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ class PlatformAsset {
|
|||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
final int? size;
|
||||
|
||||
final PlatformAssetPlaybackStyle playbackStyle;
|
||||
|
||||
const PlatformAsset({
|
||||
|
|
@ -49,6 +51,7 @@ class PlatformAsset {
|
|||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.size,
|
||||
this.playbackStyle = PlatformAssetPlaybackStyle.unknown,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import '../../fixtures/asset.stub.dart';
|
|||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../../repository.mocks.dart';
|
||||
import '../../service.mocks.dart';
|
||||
import '../../unit/factories/local_asset_factory.dart';
|
||||
|
||||
void main() {
|
||||
late LocalSyncService sut;
|
||||
|
|
@ -241,4 +242,52 @@ void main() {
|
|||
expect(localAsset.updatedAt, isNot(localAsset.createdAt));
|
||||
});
|
||||
});
|
||||
|
||||
group('assetsEqual - Android', () {
|
||||
test('ignores fields not from local sync', () {
|
||||
final fromDevice = LocalAssetFactory.create();
|
||||
final fromDb = fromDevice.copyWith(checksum: 'checksum', remoteId: 'remote', cloudId: 'cloud');
|
||||
|
||||
expect(fromDb == fromDevice, isFalse);
|
||||
expect(sut.assetsEqual(fromDb, fromDevice), isTrue);
|
||||
});
|
||||
|
||||
test('updates on size change', () {
|
||||
final asset = LocalAssetFactory.create().copyWith(size: 123);
|
||||
expect(sut.assetsEqual(asset, asset.copyWith(size: 1234)), isFalse);
|
||||
});
|
||||
|
||||
test('updates on dimension change', () {
|
||||
final asset = LocalAssetFactory.create().copyWith(width: 12, height: 34);
|
||||
expect(sut.assetsEqual(asset, asset.copyWith(width: 34, height: 12)), isFalse);
|
||||
});
|
||||
|
||||
test('updates on modified time change', () {
|
||||
final asset = LocalAssetFactory.create();
|
||||
expect(sut.assetsEqual(asset, asset.copyWith(updatedAt: asset.updatedAt.add(const .new(seconds: 1)))), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('assetsEqual - iOS', () {
|
||||
setUp(() => debugDefaultTargetPlatformOverride = .iOS);
|
||||
tearDown(() => debugDefaultTargetPlatformOverride = .android);
|
||||
|
||||
test('ignores fields not from local sync', () {
|
||||
final fromDevice = LocalAssetFactory.create();
|
||||
final fromDb = fromDevice.copyWith(checksum: 'checksum', remoteId: 'remote', cloudId: 'cloud');
|
||||
|
||||
expect(fromDb == fromDevice, isFalse);
|
||||
expect(sut.assetsEqual(fromDb, fromDevice), isTrue);
|
||||
});
|
||||
|
||||
test('updates on adjustmentTime change', () {
|
||||
final asset = LocalAssetFactory.create();
|
||||
expect(sut.assetsEqual(asset, asset.copyWith(adjustmentTime: .utc(2023, 1, 23))), isFalse);
|
||||
});
|
||||
|
||||
test('ignores the modified time', () {
|
||||
final asset = LocalAssetFactory.create();
|
||||
expect(sut.assetsEqual(asset, asset.copyWith(updatedAt: asset.updatedAt.add(const .new(seconds: 1)))), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
150
mobile/test/medium/repositories/local_album_repository_test.dart
Normal file
150
mobile/test/medium/repositories/local_album_repository_test.dart
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late DriftLocalAlbumRepository sut;
|
||||
late LocalAlbum album;
|
||||
late LocalAsset asset;
|
||||
const checksum = 'checksum';
|
||||
|
||||
Future<LocalAssetEntityData> assetInDb(String id) =>
|
||||
(ctx.db.select(ctx.db.localAssetEntity)..where((r) => r.id.equals(id))).getSingle();
|
||||
|
||||
setUp(() async {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = DriftLocalAlbumRepository(ctx.db);
|
||||
|
||||
album = (await ctx.newLocalAlbum()).toDto(assetCount: 1);
|
||||
asset = (await ctx.newLocalAsset(
|
||||
checksum: checksum,
|
||||
size: 1234,
|
||||
width: 12,
|
||||
height: 34,
|
||||
adjustmentTime: DateTime.utc(2026, 1, 2, 3),
|
||||
)).toDto();
|
||||
await sut.upsert(album, toUpsert: [asset]);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('upsert on Android', () {
|
||||
setUp(() => debugDefaultTargetPlatformOverride = .android);
|
||||
|
||||
group('resets the checksum', () {
|
||||
test('when the size changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(size: 4567)]);
|
||||
expect((await assetInDb(asset.id)).checksum, isNull);
|
||||
});
|
||||
|
||||
test('when the dimensions were swapped', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(width: 34, height: 12)]);
|
||||
expect((await assetInDb(asset.id)).checksum, isNull);
|
||||
});
|
||||
|
||||
test('when the modified time changed', () async {
|
||||
final later = asset.updatedAt.add(const .new(minutes: 1));
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(updatedAt: later)]);
|
||||
expect((await assetInDb(asset.id)).checksum, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('keeps the checksum', () {
|
||||
test('when nothing changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when only the name changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(name: 'new-name')]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when only the favourite flag changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(isFavorite: true)]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when only the created time changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(createdAt: asset.createdAt.add(const .new(days: 1)))]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
});
|
||||
|
||||
test('refreshes the other fields while keeping the checksum', () async {
|
||||
final updated = asset.copyWith(name: 'new-name', isFavorite: true);
|
||||
await sut.upsert(album, toUpsert: [updated]);
|
||||
|
||||
final row = await assetInDb(asset.id);
|
||||
expect(row.name, updated.name);
|
||||
expect(row.isFavorite, isTrue);
|
||||
expect(row.checksum, checksum);
|
||||
});
|
||||
});
|
||||
|
||||
group('upsert on iOS', () {
|
||||
setUp(() => debugDefaultTargetPlatformOverride = .iOS);
|
||||
|
||||
test('resets the checksum when the adjustment time changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(adjustmentTime: DateTime.utc(2026, 2, 3))]);
|
||||
expect((await assetInDb(asset.id)).checksum, isNull);
|
||||
});
|
||||
|
||||
group('keeps the checksum', () {
|
||||
test('when nothing changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when the modified time changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(updatedAt: asset.updatedAt.add(const .new(days: 1)))]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when the name or favorite changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(name: 'new-name', isFavorite: true)]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
|
||||
test('when the coordinates changed', () async {
|
||||
await sut.upsert(album, toUpsert: [asset.copyWith(latitude: 1, longitude: 2)]);
|
||||
expect((await assetInDb(asset.id)).checksum, checksum);
|
||||
});
|
||||
});
|
||||
|
||||
test('refreshes the other fields while keeping the checksum', () async {
|
||||
final updated = asset.copyWith(name: 'new-name', isFavorite: true);
|
||||
await sut.upsert(album, toUpsert: [updated]);
|
||||
|
||||
final row = await assetInDb(asset.id);
|
||||
expect(row.name, updated.name);
|
||||
expect(row.latitude, updated.latitude);
|
||||
expect(row.longitude, updated.longitude);
|
||||
expect(row.checksum, checksum);
|
||||
});
|
||||
});
|
||||
|
||||
group('resetSync', () {
|
||||
test('marks every album as never synced so the next sync cannot skip it', () async {
|
||||
final other = await ctx.newLocalAlbum();
|
||||
await sut.resetSync();
|
||||
|
||||
final rows = await ctx.db.select(ctx.db.localAlbumEntity).get();
|
||||
expect(rows, hasLength(2));
|
||||
expect(rows.map((a) => a.updatedAt), everyElement(kLocalAlbumNeverSynced));
|
||||
expect(rows.map((a) => a.id), containsAll([album.id, other.id]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -279,6 +279,7 @@ class MediumRepositoryContext {
|
|||
int? height,
|
||||
int? durationMs,
|
||||
int? orientation,
|
||||
int? size,
|
||||
DateTime? updatedAt,
|
||||
}) async {
|
||||
id ??= TestUtils.uuid();
|
||||
|
|
@ -292,6 +293,7 @@ class MediumRepositoryContext {
|
|||
width: .new(width ?? TestUtils.randInt(1000)),
|
||||
durationMs: .new(durationMs ?? 0),
|
||||
orientation: .new(orientation ?? 0),
|
||||
size: .new(size),
|
||||
updatedAt: .new(TestUtils.date(updatedAt)),
|
||||
checksum: _resolveUndefined(checksum, checksumOption, const Uuid().v4()),
|
||||
createdAt: .new(TestUtils.date(createdAt)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue