This commit is contained in:
Priyanshu 2026-08-15 12:41:07 +06:00 committed by GitHub
commit 1e5d5ef65d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 210 additions and 28 deletions

View file

@ -34,7 +34,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.isIn([AssetVisibility.archive.index, AssetVisibility.timeline.index]),
useColumns: false,
),
leftOuterJoin(
@ -94,7 +95,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.isIn([
AssetVisibility.archive.index,
AssetVisibility.timeline.index,
]),
useColumns: false,
),
leftOuterJoin(
@ -218,16 +223,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
}
Stream<(DateTime, DateTime)> watchDateRange(String albumId) {
final dateExp = coalesce([_db.remoteAssetEntity.localDateTime, _db.remoteAssetEntity.createdAt]);
final query = _db.remoteAlbumAssetEntity.selectOnly()
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..addColumns([_db.remoteAssetEntity.createdAt.min(), _db.remoteAssetEntity.createdAt.max()])
..addColumns([dateExp.min(), dateExp.max()])
..join([
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.isIn([AssetVisibility.archive.index, AssetVisibility.timeline.index]),
),
]);
return query.map((row) {
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
final minDate = row.read(dateExp.min());
final maxDate = row.read(dateExp.max());
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
}).watchSingle();
}
@ -419,16 +430,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
'''
SELECT
raae.album_id,
$sqlAgg(rae.local_date_time) AS asset_date
$sqlAgg(COALESCE(rae.local_date_time, rae.created_at)) AS asset_date
FROM json_each(?) ids
INNER JOIN remote_album_asset_entity raae
ON raae.album_id = ids.value
INNER JOIN remote_asset_entity rae
ON rae.id = raae.asset_id
WHERE rae.deleted_at IS NULL
AND rae.visibility IN (?, ?)
GROUP BY raae.album_id
ORDER BY asset_date ASC
''',
variables: [Variable<String>(jsonIds)],
variables: [
Variable<String>(jsonIds),
Variable<int>(AssetVisibility.timeline.index),
Variable<int>(AssetVisibility.archive.index),
],
readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity},
)
.get();
@ -530,7 +547,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull(),
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.isIn([
AssetVisibility.archive.index,
AssetVisibility.timeline.index,
]),
useColumns: false,
),
leftOuterJoin(

View file

@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import '../repository_context.dart';
@ -86,6 +87,24 @@ void main() {
expect(albums.first.id, album.id);
expect(albums.first.assetCount, 0);
});
test('excludes non-default visibility assets (hidden/locked) from assetCount', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final active = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.timeline);
final archived = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.archive);
final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden);
final locked = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.locked);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: archived.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: locked.id);
final albums = await sut.getAll();
expect(albums, hasLength(1));
expect(albums.first.assetCount, 2);
});
});
group('get', () {
@ -101,6 +120,20 @@ void main() {
expect(result?.id, album.id);
expect(result?.assetCount, 0);
});
test('excludes non-default visibility assets (hidden/locked) from assetCount', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final active = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.timeline);
final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id);
final result = await sut.get(album.id);
expect(result, isNotNull);
expect(result?.assetCount, 1);
});
});
group('getAlbumsContainingAsset', () {
@ -130,6 +163,87 @@ void main() {
expect(albums, hasLength(1));
expect(albums.first.assetCount, 0);
});
test('excludes non-default visibility assets from assetCount', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final active = await ctx.newRemoteAsset(ownerId: user.id);
final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id);
final albums = await sut.getAlbumsContainingAsset(active.id);
expect(albums, hasLength(1));
expect(albums.first.assetCount, 1);
});
});
group('watchDateRange', () {
test('excludes trashed and hidden/locked assets from date range calculation', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final trashed = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2023, 1, 1),
deletedAt: DateTime(2025, 1, 1),
);
final active1 = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2024, 5, 10),
localDateTime: DateTime(2024, 5, 1),
visibility: AssetVisibility.timeline,
);
final active2 = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2024, 10, 10),
localDateTime: DateTime(2024, 10, 1),
visibility: AssetVisibility.archive,
);
final hidden = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2024, 12, 1),
visibility: AssetVisibility.hidden,
);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: trashed.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active1.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active2.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id);
final range = await sut.watchDateRange(album.id).first;
expect(range.$1.toUtc(), active1.localDateTime!.toUtc());
expect(range.$2.toUtc(), active2.localDateTime!.toUtc());
});
test('prioritizes localDateTime over createdAt for date range calculation', () async {
final user = await ctx.newUser();
final album = await ctx.newRemoteAlbum(ownerId: user.id);
final asset1 = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2024, 5, 10),
localDateTime: DateTime(2024, 5, 1),
visibility: AssetVisibility.timeline,
);
final asset2 = await ctx.newRemoteAsset(
ownerId: user.id,
createdAt: DateTime(2024, 10, 10),
localDateTime: DateTime(2024, 10, 1),
visibility: AssetVisibility.archive,
);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset1.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset2.id);
final range = await sut.watchDateRange(album.id).first;
// Dates should match localDateTime, not createdAt
expect(range.$1.toUtc(), DateTime(2024, 5, 1).toUtc());
expect(range.$2.toUtc(), DateTime(2024, 10, 1).toUtc());
});
});
group('getSortedAlbumIds', () {
@ -321,5 +435,51 @@ void main() {
// album2 (Jan 1) should come before album1 (Jan 25)
expect(resultEnd, [album2.id, album1.id]);
});
test('ignores trashed assets when sorting albums by date', () async {
// Album 1: one active asset at Jan 10, one trashed asset at Jan 1 (older)
final album1 = await ctx.newRemoteAlbum(ownerId: userId);
final trashedEarly = await ctx.newRemoteAsset(
ownerId: userId,
createdAt: DateTime(2024, 1, 1),
deletedAt: DateTime(2025, 1, 1),
);
final active = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10));
await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: trashedEarly.id);
await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: active.id);
// Album 2: one active asset at Jan 5 (earlier than album1's active asset)
final album2 = await ctx.newRemoteAlbum(ownerId: userId);
final active2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5));
await ctx.newRemoteAlbumAsset(albumId: album2.id, assetId: active2.id);
final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start);
// album2 (Jan 5) should come before album1 (Jan 10, ignoring trashed Jan 1)
expect(result, [album2.id, album1.id]);
});
test('ignores hidden and locked assets when sorting albums by date', () async {
// Album 1: active asset at Jan 20, hidden asset at Jan 1 (should be ignored)
final album1 = await ctx.newRemoteAlbum(ownerId: userId);
final hiddenEarly = await ctx.newRemoteAsset(
ownerId: userId,
createdAt: DateTime(2024, 1, 1),
visibility: AssetVisibility.hidden,
);
final activeAlbum1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20));
await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: hiddenEarly.id);
await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: activeAlbum1.id);
// Album 2: one active asset at Jan 10 (earlier than album1's visible start)
final album2 = await ctx.newRemoteAlbum(ownerId: userId);
final activeAlbum2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10));
await ctx.newRemoteAlbumAsset(albumId: album2.id, assetId: activeAlbum2.id);
final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start);
// album2 (Jan 10) before album1 (Jan 20, ignoring hidden Jan 1)
expect(result, [album2.id, album1.id]);
});
});
}

View file

@ -121,6 +121,7 @@ class MediumRepositoryContext {
String? stackId,
String? thumbHash,
String? libraryId,
DateTime? localDateTime,
}) async {
id ??= TestUtils.uuid();
createdAt ??= TestUtils.date();
@ -128,25 +129,25 @@ class MediumRepositoryContext {
.into(db.remoteAssetEntity)
.insertReturning(
RemoteAssetEntityCompanion(
id: .new(id),
name: .new('remote_$id.jpg'),
checksum: .new(TestUtils.uuid(checksum)),
type: .new(type ?? .image),
createdAt: .new(createdAt),
updatedAt: .new(TestUtils.date(updatedAt)),
ownerId: .new(TestUtils.uuid(ownerId)),
visibility: .new(visibility ?? .timeline),
deletedAt: .new(deletedAt),
durationMs: .new(durationMs ?? 0),
width: .new(width ?? TestUtils.randInt(1000)),
height: .new(height ?? TestUtils.randInt(1000)),
isFavorite: .new(isFavorite ?? false),
isEdited: .new(isEdited ?? false),
livePhotoVideoId: .new(livePhotoVideoId),
stackId: .new(stackId),
localDateTime: .new(createdAt.toLocal()),
thumbHash: .new(TestUtils.uuid(thumbHash)),
libraryId: .new(TestUtils.uuid(libraryId)),
id: Value(id),
name: Value('remote_$id.jpg'),
checksum: Value(TestUtils.uuid(checksum)),
type: Value(type ?? AssetType.image),
createdAt: Value(createdAt),
updatedAt: Value(TestUtils.date(updatedAt)),
ownerId: Value(TestUtils.uuid(ownerId)),
visibility: Value(visibility ?? AssetVisibility.timeline),
deletedAt: Value(deletedAt),
durationMs: Value(durationMs ?? 0),
width: Value(width ?? TestUtils.randInt(1000)),
height: Value(height ?? TestUtils.randInt(1000)),
isFavorite: Value(isFavorite ?? false),
isEdited: Value(isEdited ?? false),
livePhotoVideoId: Value(livePhotoVideoId),
stackId: Value(stackId),
localDateTime: Value(localDateTime ?? createdAt.toLocal()),
thumbHash: Value(TestUtils.uuid(thumbHash)),
libraryId: Value(TestUtils.uuid(libraryId)),
),
);
}