mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
fix(mobile): sort timeline assets by the date their headers group on
This commit is contained in:
parent
7e70f90c15
commit
f600aa3151
17 changed files with 4727 additions and 41 deletions
3692
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
3692
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,9 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
|||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)')
|
||||
@TableIndex.sql(
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_group ON local_asset_entity (group_date DESC, created_at DESC)',
|
||||
)
|
||||
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
const LocalAssetEntity();
|
||||
|
||||
|
|
@ -22,6 +25,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
|
||||
TextColumn get groupDate => text().nullable()();
|
||||
|
||||
RealColumn get latitude => real().nullable()();
|
||||
|
||||
RealColumn get longitude => real().nullable()();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ SELECT
|
|||
NULL as adjustmentTime,
|
||||
rae.is_edited,
|
||||
0 as playback_style,
|
||||
rae.uploaded_at
|
||||
rae.uploaded_at,
|
||||
rae.group_date
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
|
|
@ -37,6 +38,7 @@ WHERE
|
|||
rae.deleted_at IS NULL
|
||||
AND rae.visibility = 0 -- timeline visibility
|
||||
AND rae.owner_id IN :user_ids
|
||||
AND rae.group_date IS NOT NULL
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
OR rae.id = se.primary_asset_id
|
||||
|
|
@ -67,7 +69,8 @@ SELECT
|
|||
lae.adjustment_time,
|
||||
0 as is_edited,
|
||||
lae.playback_style,
|
||||
NULL as uploaded_at
|
||||
NULL as uploaded_at,
|
||||
lae.group_date
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
|
@ -83,7 +86,8 @@ AND NOT EXISTS (
|
|||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
AND lae.group_date IS NOT NULL
|
||||
ORDER BY group_date DESC, created_at DESC
|
||||
LIMIT $limit;
|
||||
|
||||
mergedBucket(:group_by AS INTEGER):
|
||||
|
|
@ -94,14 +98,8 @@ FROM
|
|||
(
|
||||
SELECT
|
||||
CASE
|
||||
WHEN :group_by = 0 THEN COALESCE(
|
||||
STRFTIME('%Y-%m-%d', rae.local_date_time),
|
||||
STRFTIME('%Y-%m-%d', rae.created_at, 'localtime')
|
||||
)
|
||||
WHEN :group_by = 1 THEN COALESCE(
|
||||
STRFTIME('%Y-%m', rae.local_date_time),
|
||||
STRFTIME('%Y-%m', rae.created_at, 'localtime')
|
||||
)
|
||||
WHEN :group_by = 0 THEN rae.group_date
|
||||
WHEN :group_by = 1 THEN SUBSTR(rae.group_date, 1, 7)
|
||||
END as bucket_date
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
|
|
@ -111,6 +109,7 @@ FROM
|
|||
rae.deleted_at IS NULL
|
||||
AND rae.visibility = 0 -- timeline visibility
|
||||
AND rae.owner_id in :user_ids
|
||||
AND rae.group_date IS NOT NULL
|
||||
AND (
|
||||
rae.stack_id IS NULL
|
||||
OR rae.id = se.primary_asset_id
|
||||
|
|
@ -118,8 +117,8 @@ FROM
|
|||
UNION ALL
|
||||
SELECT
|
||||
CASE
|
||||
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', lae.created_at, 'localtime')
|
||||
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', lae.created_at, 'localtime')
|
||||
WHEN :group_by = 0 THEN lae.group_date
|
||||
WHEN :group_by = 1 THEN SUBSTR(lae.group_date, 1, 7)
|
||||
END as bucket_date
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
|
|
@ -136,6 +135,7 @@ FROM
|
|||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
|
||||
)
|
||||
AND lae.group_date IS NOT NULL
|
||||
)
|
||||
GROUP BY bucket_date
|
||||
ORDER BY bucket_date DESC;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ WHERE (library_id IS NOT NULL);
|
|||
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
||||
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
||||
''')
|
||||
@TableIndex.sql('''
|
||||
CREATE INDEX IF NOT EXISTS idx_remote_asset_group
|
||||
ON remote_asset_entity (owner_id, visibility, deleted_at, group_date DESC, created_at DESC)
|
||||
''')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)')
|
||||
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
const RemoteAssetEntity();
|
||||
|
|
@ -35,6 +39,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
|||
|
||||
DateTimeColumn get localDateTime => dateTime().nullable()();
|
||||
|
||||
TextColumn get groupDate => text().nullable()();
|
||||
|
||||
TextColumn get thumbHash => text().nullable()();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
|
|
|||
|
|
@ -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,12 @@ class Drift extends $Drift {
|
|||
from30To31: (m, v31) async {
|
||||
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||
},
|
||||
from31To32: (m, v32) async {
|
||||
await m.addColumn(v32.remoteAssetEntity, v32.remoteAssetEntity.groupDate);
|
||||
await m.addColumn(v32.localAssetEntity, v32.localAssetEntity.groupDate);
|
||||
await m.createIndex(v32.idxRemoteAssetGroup);
|
||||
await m.createIndex(v32.idxLocalAssetGroup);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16506,6 +16506,696 @@ 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,
|
||||
idxLocalAssetGroup,
|
||||
idxStackPrimaryAssetId,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||
idxRemoteAssetGroup,
|
||||
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 Shape52 remoteAssetEntity = Shape52(
|
||||
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_225,
|
||||
_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 Shape53 localAssetEntity = Shape53(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_225,
|
||||
_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 idxLocalAssetGroup = i1.Index(
|
||||
'idx_local_asset_group',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_group ON local_asset_entity (group_date DESC, created_at DESC)',
|
||||
);
|
||||
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 idxRemoteAssetGroup = i1.Index(
|
||||
'idx_remote_asset_group',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_group ON remote_asset_entity (owner_id, visibility, deleted_at, group_date DESC, 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<String> get ownerId =>
|
||||
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get localDateTime =>
|
||||
columnsByName['local_date_time']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get groupDate =>
|
||||
columnsByName['group_date']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get thumbHash =>
|
||||
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get uploadedAt =>
|
||||
columnsByName['uploaded_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get visibility =>
|
||||
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get stackId =>
|
||||
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get libraryId =>
|
||||
columnsByName['library_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isEdited =>
|
||||
columnsByName['is_edited']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_225(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'group_date',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
|
||||
class Shape53 extends i0.VersionedTable {
|
||||
Shape53({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<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<String> get groupDate =>
|
||||
columnsByName['group_date']! 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>;
|
||||
}
|
||||
|
||||
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 +17227,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 +17381,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 +17423,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 +17456,6 @@ i1.OnUpgrade stepByStep({
|
|||
from28To29: from28To29,
|
||||
from29To30: from29To30,
|
||||
from30To31: from30To31,
|
||||
from31To32: from31To32,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.d
|
|||
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/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
|
||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||
|
||||
|
|
@ -307,6 +308,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||
latitude: Value(asset.latitude),
|
||||
longitude: Value(asset.longitude),
|
||||
adjustmentTime: Value(asset.adjustmentTime),
|
||||
groupDate: Value(timelineGroupDate(asset.createdAt.toLocal())),
|
||||
);
|
||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||
_db.localAssetEntity,
|
||||
|
|
@ -337,6 +339,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||
orientation: Value(asset.orientation),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
playbackStyle: Value(asset.playbackStyle),
|
||||
groupDate: Value(timelineGroupDate(asset.createdAt.toLocal())),
|
||||
);
|
||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||
_db.localAssetEntity,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
|
||||
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||
|
||||
|
|
@ -327,6 +328,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||
isFavorite: Value(source.isFavorite),
|
||||
visibility: const Value(AssetVisibility.timeline),
|
||||
isEdited: Value(source.isEdited),
|
||||
groupDate: Value(remoteGroupDate(null, source.createdAt)),
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
|
|
@ -292,6 +293,9 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||
}
|
||||
|
||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime, {String? timeZone}) {
|
||||
// the picker value arrives as UTC; its wall day lives in the offset
|
||||
final offset = tryParseUtcOffset(timeZone);
|
||||
final groupDate = timelineGroupDate(offset != null ? dateTime.toUtc().add(offset) : dateTime);
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
batch.update(
|
||||
|
|
@ -304,7 +308,7 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
batch.update(
|
||||
_db.remoteAssetEntity,
|
||||
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
|
||||
RemoteAssetEntityCompanion(createdAt: Value(dateTime), groupDate: Value(groupDate)),
|
||||
where: (e) => e.id.equals(id),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey;
|
||||
import 'package:openapi/api.dart' hide AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey;
|
||||
|
|
@ -210,6 +211,9 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
isFavorite: Value(asset.isFavorite),
|
||||
ownerId: Value(asset.ownerId),
|
||||
localDateTime: Value(asset.localDateTime),
|
||||
groupDate: asset.localDateTime == null && asset.fileCreatedAt == null
|
||||
? const Value.absent()
|
||||
: Value(remoteGroupDate(asset.localDateTime, asset.fileCreatedAt!)),
|
||||
thumbHash: Value(asset.thumbhash),
|
||||
deletedAt: Value(asset.deletedAt),
|
||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||
|
|
@ -249,6 +253,9 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||
isFavorite: Value(asset.isFavorite),
|
||||
ownerId: Value(asset.ownerId),
|
||||
localDateTime: Value(asset.localDateTime),
|
||||
groupDate: asset.localDateTime == null && asset.fileCreatedAt == null
|
||||
? const Value.absent()
|
||||
: Value(remoteGroupDate(asset.localDateTime, asset.fileCreatedAt!)),
|
||||
thumbHash: Value(asset.thumbhash),
|
||||
deletedAt: Value(asset.deletedAt),
|
||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||
);
|
||||
|
||||
Stream<List<Bucket>> _watchMainBucket(List<String> userIds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
|
||||
if (groupBy == GroupAssetsBy.none) {
|
||||
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
|
||||
if (groupBy == GroupAssetsBy.none || groupBy == GroupAssetsBy.auto) {
|
||||
throw UnsupportedError("$groupBy is not supported for watchMainBucket");
|
||||
}
|
||||
|
||||
return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
|
||||
final date = row.bucketDate.truncateDate(groupBy);
|
||||
final date = row.bucketDate!.truncateDate(groupBy);
|
||||
return TimeBucket(date: date, assetCount: row.assetCount);
|
||||
}).watch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.d
|
|||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
|
||||
typedef TrashedAsset = ({String albumId, LocalAsset asset});
|
||||
|
||||
|
|
@ -198,6 +199,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
isFavorite: Value(e.isFavorite),
|
||||
orientation: Value(e.orientation),
|
||||
playbackStyle: Value(e.playbackStyle),
|
||||
groupDate: Value(timelineGroupDate(e.createdAt.toLocal())),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,26 @@ DateTime? tryFromSecondsSinceEpoch(int? secondsSinceEpoch, {bool isUtc = false})
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Uses the held components, so convert with toLocal() first for instants.
|
||||
// Dates sqlite cannot read (year outside 1..9999) fall to null instead of a mangled day
|
||||
String? timelineGroupDate(DateTime value) {
|
||||
if (value.year < 1 || value.year > 9999) {
|
||||
return null;
|
||||
}
|
||||
return value.toIso8601String().split('T').first;
|
||||
}
|
||||
|
||||
// 'UTC+14:00' style, from the date picker path
|
||||
Duration? tryParseUtcOffset(String? value) {
|
||||
final match = value == null ? null : RegExp(r'^UTC([+-])(\d{2}):(\d{2})$').firstMatch(value);
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
final minutes = int.parse(match[2]!) * 60 + int.parse(match[3]!);
|
||||
return Duration(minutes: match[1] == '-' ? -minutes : minutes);
|
||||
}
|
||||
|
||||
// group_date for remote rows: wall day when known, else the local day of createdAt
|
||||
String? remoteGroupDate(DateTime? localDateTime, DateTime createdAt) =>
|
||||
(localDateTime != null ? timelineGroupDate(localDateTime) : null) ?? timelineGroupDate(createdAt.toLocal());
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da
|
|||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const int targetVersion = 26;
|
||||
const int targetVersion = 28;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int? storedVersion = Store.tryGet(StoreKey.version);
|
||||
|
|
@ -34,6 +34,10 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
|||
await _migrateTo26(drift);
|
||||
}
|
||||
|
||||
if (version < 28) {
|
||||
await _migrateTo28(drift);
|
||||
}
|
||||
|
||||
if (storedVersion == null) {
|
||||
await FeatureMessageService(SettingsRepository.instance).markSeen();
|
||||
}
|
||||
|
|
@ -42,6 +46,19 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
|||
return;
|
||||
}
|
||||
|
||||
Future<void> _migrateTo28(Drift drift) => backfillAssetGroupDates(drift);
|
||||
|
||||
// Store-level on purpose: runs after the date heals in this chain, so group_date is
|
||||
// computed from the corrected values. STRFTIME drops dates sqlite cannot read.
|
||||
Future<void> backfillAssetGroupDates(Drift drift) async {
|
||||
await drift.customStatement(
|
||||
"UPDATE remote_asset_entity SET group_date = COALESCE(STRFTIME('%Y-%m-%d', local_date_time), STRFTIME('%Y-%m-%d', created_at, 'localtime'))",
|
||||
);
|
||||
await drift.customStatement(
|
||||
"UPDATE local_asset_entity SET group_date = STRFTIME('%Y-%m-%d', created_at, 'localtime')",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _migrateTo25() async {
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
if (accessToken == null || accessToken.isEmpty) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
import 'generated/schema.dart';
|
||||
import 'generated/schema_v1.dart' as v1;
|
||||
|
|
@ -35,4 +39,78 @@ void main() {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('v32 group_date backfill', () {
|
||||
// 26479 class: platform handed us a date sqlite cannot read. the bucket stream
|
||||
// must not die, and the broken row must vanish from buckets and assets alike.
|
||||
test(
|
||||
'garbage created_at survives migration and is filtered from the timeline',
|
||||
() async {
|
||||
await initializeDateFormatting();
|
||||
final schema = await verifier.schemaAt(31);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_album_entity (id, name, backup_selection) VALUES ('album-1', 'Camera', 0)",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_asset_entity (id, name, type, created_at, updated_at) VALUES ('garbage', 'g.jpg', 1, '57780-01-01T00:00:00.000Z', '57780-01-01T00:00:00.000Z')",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_asset_entity (id, name, type, created_at, updated_at) VALUES ('good', 'ok.jpg', 1, '2026-07-24T10:00:00.000Z', '2026-07-24T10:00:00.000Z')",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_album_asset_entity (asset_id, album_id) VALUES ('garbage', 'album-1')",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_album_asset_entity (asset_id, album_id) VALUES ('good', 'album-1')",
|
||||
);
|
||||
|
||||
final db = Drift(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 32);
|
||||
await backfillAssetGroupDates(db);
|
||||
|
||||
final repo = DriftTimelineRepository(db);
|
||||
final buckets = await repo
|
||||
.main(const ['user-1'], GroupAssetsBy.day)
|
||||
.bucketSource()
|
||||
.first;
|
||||
expect(buckets, hasLength(1));
|
||||
expect(buckets.single.assetCount, 1);
|
||||
expect((buckets.single as TimeBucket).date, DateTime(2026, 7, 24));
|
||||
await db.close();
|
||||
},
|
||||
);
|
||||
|
||||
// The drift migration only adds the column; the backfill is a store-level step so a
|
||||
// created_at heal (29193) that runs before it actually lands in group_date.
|
||||
test('a created_at heal before the backfill lands in the header day', () async {
|
||||
await initializeDateFormatting();
|
||||
final schema = await verifier.schemaAt(31);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_album_entity (id, name, backup_selection) VALUES ('album-1', 'Camera', 0)",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_asset_entity (id, name, type, created_at, updated_at) VALUES ('healed', 'h.jpg', 1, '2027-01-01T00:00:00.000Z', '2026-07-20T10:00:00.000Z')",
|
||||
);
|
||||
schema.rawDatabase.execute(
|
||||
"INSERT INTO local_album_asset_entity (asset_id, album_id) VALUES ('healed', 'album-1')",
|
||||
);
|
||||
|
||||
final db = Drift(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 32);
|
||||
|
||||
// 29193's store-level heal
|
||||
await db.customStatement(
|
||||
"UPDATE local_asset_entity SET created_at = updated_at WHERE julianday(created_at) > julianday(updated_at)",
|
||||
);
|
||||
await backfillAssetGroupDates(db);
|
||||
|
||||
final repo = DriftTimelineRepository(db);
|
||||
final buckets = await repo
|
||||
.main(const ['user-1'], GroupAssetsBy.day)
|
||||
.bucketSource()
|
||||
.first;
|
||||
expect((buckets.single as TimeBucket).date, DateTime(2026, 7, 20));
|
||||
await db.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,191 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
|
||||
const _userId = 'user-1';
|
||||
const _albumId = 'album-1';
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
|
||||
setUp(() {
|
||||
setUp(() async {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await db
|
||||
.into(db.userEntity)
|
||||
.insert(UserEntityCompanion.insert(id: _userId, email: 'user-1@test.dev', name: 'User 1'));
|
||||
await db
|
||||
.into(db.localAlbumEntity)
|
||||
.insert(
|
||||
LocalAlbumEntityCompanion.insert(id: _albumId, name: 'Camera', backupSelection: BackupSelection.selected),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('mergedBucket falls back to createdAt when localDateTime is null', () async {
|
||||
const userId = 'user-1';
|
||||
final createdAt = DateTime(2024, 1, 1, 12);
|
||||
Future<void> insertRemote(String id, {required DateTime createdAt, DateTime? localDateTime}) => db
|
||||
.into(db.remoteAssetEntity)
|
||||
.insert(
|
||||
RemoteAssetEntityCompanion.insert(
|
||||
id: id,
|
||||
name: '$id.jpg',
|
||||
type: AssetType.image,
|
||||
checksum: 'checksum-$id',
|
||||
ownerId: _userId,
|
||||
visibility: AssetVisibility.timeline,
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(createdAt),
|
||||
uploadedAt: Value(createdAt),
|
||||
localDateTime: Value(localDateTime),
|
||||
groupDate: Value(remoteGroupDate(localDateTime, createdAt)),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> insertLocal(String id, {required DateTime createdAt}) async {
|
||||
await db
|
||||
.into(db.userEntity)
|
||||
.insert(UserEntityCompanion.insert(id: userId, email: 'user-1@test.dev', name: 'User 1'));
|
||||
|
||||
await db
|
||||
.into(db.remoteAssetEntity)
|
||||
.into(db.localAssetEntity)
|
||||
.insert(
|
||||
RemoteAssetEntityCompanion.insert(
|
||||
id: 'asset-1',
|
||||
name: 'asset-1.jpg',
|
||||
LocalAssetEntityCompanion.insert(
|
||||
id: id,
|
||||
name: '$id.jpg',
|
||||
type: AssetType.image,
|
||||
checksum: 'checksum-1',
|
||||
ownerId: userId,
|
||||
visibility: AssetVisibility.timeline,
|
||||
checksum: Value('checksum-$id'),
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(createdAt),
|
||||
uploadedAt: Value(createdAt),
|
||||
localDateTime: const Value(null),
|
||||
groupDate: Value(timelineGroupDate(createdAt.toLocal())),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.into(db.localAlbumAssetEntity)
|
||||
.insert(LocalAlbumAssetEntityCompanion.insert(assetId: id, albumId: _albumId));
|
||||
}
|
||||
|
||||
final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId]).get();
|
||||
// Mirrors how the timeline pairs headers with tiles: buckets only carry a count, the assets
|
||||
// come from one flat list that is addressed by the running offset of the previous buckets.
|
||||
Future<List<(String, String)>> headerForEachAsset(GroupAssetsBy groupBy) async {
|
||||
final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: groupBy.index, userIds: [_userId]).get();
|
||||
final assets = await db.mergedAssetDrift.mergedAsset(userIds: [_userId], limit: (_) => Limit(1000, 0)).get();
|
||||
|
||||
expect(buckets, hasLength(1));
|
||||
expect(buckets.single.assetCount, 1);
|
||||
expect(buckets.single.bucketDate, isNotEmpty);
|
||||
final pairs = <(String, String)>[];
|
||||
var offset = 0;
|
||||
for (final bucket in buckets) {
|
||||
for (final asset in assets.skip(offset).take(bucket.assetCount)) {
|
||||
pairs.add(((asset.remoteId ?? asset.localId)!, bucket.bucketDate!));
|
||||
}
|
||||
offset += bucket.assetCount;
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
// Regression for #29864: buckets group by localDateTime but assets were ordered by createdAt,
|
||||
// so a header minted from one asset's localDateTime was rendered above a different asset.
|
||||
group('mergedAsset ordering matches mergedBucket grouping', () {
|
||||
Future<void> seedGhost() async {
|
||||
await insertRemote('asset-a', createdAt: DateTime(2026, 4, 26, 10));
|
||||
await insertRemote('asset-b', createdAt: DateTime(2026, 4, 26, 12));
|
||||
await insertRemote('ghost', createdAt: DateTime(2026, 4, 26, 11), localDateTime: DateTime.utc(2027, 3, 4, 12));
|
||||
}
|
||||
|
||||
test('asset under each header belongs to that header date', () async {
|
||||
await seedGhost();
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.day), [
|
||||
('ghost', '2027-03-04'),
|
||||
('asset-b', '2026-04-26'),
|
||||
('asset-a', '2026-04-26'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('deleting the asset under a header removes the empty header', () async {
|
||||
await seedGhost();
|
||||
|
||||
await db.remoteAssetEntity.deleteWhere((row) => row.id.equals('ghost'));
|
||||
|
||||
final buckets = await db.mergedAssetDrift
|
||||
.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [_userId])
|
||||
.get();
|
||||
expect(buckets.map((b) => b.bucketDate), ['2026-04-26']);
|
||||
expect(buckets.single.assetCount, 2);
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.day), [('asset-b', '2026-04-26'), ('asset-a', '2026-04-26')]);
|
||||
});
|
||||
|
||||
// Web-consistency: the server groups on the date only and sorts within a day by
|
||||
// createdAt, so two assets whose wall clock and createdAt disagree within the same
|
||||
// day must come out in createdAt order.
|
||||
test('same-day assets order by createdAt like the web timeline', () async {
|
||||
await insertRemote(
|
||||
'shot-late-upload-early',
|
||||
createdAt: DateTime.utc(2026, 7, 24, 1),
|
||||
localDateTime: DateTime.utc(2026, 7, 24, 22),
|
||||
);
|
||||
await insertRemote(
|
||||
'shot-early-upload-late',
|
||||
createdAt: DateTime.utc(2026, 7, 24, 15),
|
||||
localDateTime: DateTime.utc(2026, 7, 24, 8),
|
||||
);
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.day), [
|
||||
('shot-early-upload-late', '2026-07-24'),
|
||||
('shot-late-upload-early', '2026-07-24'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('device only assets are interleaved with remote assets by the date shown', () async {
|
||||
await insertRemote('remote', createdAt: DateTime.utc(2024, 4, 26, 3), localDateTime: DateTime.utc(2024, 4, 26, 10));
|
||||
await insertLocal('device', createdAt: DateTime(2024, 4, 26, 14));
|
||||
await insertRemote('older', createdAt: DateTime.utc(2024, 4, 25, 3), localDateTime: DateTime.utc(2024, 4, 25, 10));
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.day), [
|
||||
('device', '2024-04-26'),
|
||||
('remote', '2024-04-26'),
|
||||
('older', '2024-04-25'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('month grouping keeps every asset under its own month header', () async {
|
||||
await insertRemote('may', createdAt: DateTime.utc(2024, 4, 30, 23), localDateTime: DateTime.utc(2024, 5, 1, 8));
|
||||
await insertRemote('april', createdAt: DateTime.utc(2024, 5, 1, 1), localDateTime: DateTime.utc(2024, 4, 30, 20));
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.month), [('may', '2024-05'), ('april', '2024-04')]);
|
||||
});
|
||||
|
||||
// Store-migration order: the 29193 heal runs first, the group_date backfill after it,
|
||||
// so the corrected date is what lands under the header.
|
||||
test('a created_at heal before the backfill lands in the header day', () async {
|
||||
await db
|
||||
.into(db.localAssetEntity)
|
||||
.insert(
|
||||
LocalAssetEntityCompanion.insert(
|
||||
id: 'healed',
|
||||
name: 'healed.jpg',
|
||||
type: AssetType.image,
|
||||
createdAt: Value(DateTime.utc(2027)),
|
||||
updatedAt: Value(DateTime.utc(2026, 7, 20)),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.into(db.localAlbumAssetEntity)
|
||||
.insert(LocalAlbumAssetEntityCompanion.insert(assetId: 'healed', albumId: _albumId));
|
||||
|
||||
await db.customStatement(
|
||||
"UPDATE local_asset_entity SET created_at = updated_at WHERE julianday(created_at) > julianday(updated_at)",
|
||||
);
|
||||
await backfillAssetGroupDates(db);
|
||||
|
||||
expect(await headerForEachAsset(GroupAssetsBy.day), [('healed', '2026-07-20')]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
|
|
@ -126,6 +127,7 @@ class MediumRepositoryContext {
|
|||
livePhotoVideoId: .new(livePhotoVideoId),
|
||||
stackId: .new(stackId),
|
||||
localDateTime: .new(createdAt.toLocal()),
|
||||
groupDate: .new(timelineGroupDate(createdAt.toLocal())),
|
||||
thumbHash: .new(TestUtils.uuid(thumbHash)),
|
||||
libraryId: .new(TestUtils.uuid(libraryId)),
|
||||
),
|
||||
|
|
@ -263,6 +265,7 @@ class MediumRepositoryContext {
|
|||
DateTime? updatedAt,
|
||||
}) async {
|
||||
id ??= TestUtils.uuid();
|
||||
createdAt ??= TestUtils.date();
|
||||
return db
|
||||
.into(db.localAssetEntity)
|
||||
.insertReturning(
|
||||
|
|
@ -275,7 +278,8 @@ class MediumRepositoryContext {
|
|||
orientation: .new(orientation ?? 0),
|
||||
updatedAt: .new(TestUtils.date(updatedAt)),
|
||||
checksum: _resolveUndefined(checksum, checksumOption, const Uuid().v4()),
|
||||
createdAt: .new(TestUtils.date(createdAt)),
|
||||
createdAt: .new(createdAt),
|
||||
groupDate: .new(timelineGroupDate(createdAt.toLocal())),
|
||||
type: .new(type ?? .image),
|
||||
isFavorite: .new(isFavorite ?? false),
|
||||
iCloudId: .new(TestUtils.uuid(iCloudId)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue