mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 11:09:21 +03:00
perf(mobile): optimized album sorting (#25179)
* perf(mobile): optimized album sorting * refactor: add index & sql query * fix: migration * refactor: enum, ordering & list * test: update album service tests * chore: fix enums broken during merging main * chore: remove unnecessary tests * test: add tests for getSortedAlbumIds * test: added back stubs in service test
This commit is contained in:
@@ -18,3 +18,5 @@ enum ActionSource { timeline, viewer }
|
|||||||
enum CleanupStep { selectDate, scan, delete }
|
enum CleanupStep { selectDate, scan, delete }
|
||||||
|
|
||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
|
enum AssetDateAggregation { start, end }
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class RemoteAlbumService {
|
|||||||
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
AlbumSortMode.title => albums.sortedBy((album) => album.name),
|
||||||
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt),
|
||||||
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount),
|
||||||
AlbumSortMode.mostRecent => await _sortByNewestAsset(albums),
|
AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end),
|
||||||
AlbumSortMode.mostOldest => await _sortByOldestAsset(albums),
|
AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start),
|
||||||
};
|
};
|
||||||
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder;
|
||||||
|
|
||||||
@@ -172,46 +172,25 @@ class RemoteAlbumService {
|
|||||||
return _repository.getAlbumsContainingAsset(assetId);
|
return _repository.getAlbumsContainingAsset(assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
Future<List<RemoteAlbum>> _sortByAssetDate(
|
||||||
// map album IDs to their newest asset dates
|
List<RemoteAlbum> albums, {
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
required AssetDateAggregation aggregation,
|
||||||
for (final album in albums) {
|
}) async {
|
||||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
if (albums.isEmpty) return [];
|
||||||
|
|
||||||
|
final albumIds = albums.map((e) => e.id).toList();
|
||||||
|
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
|
||||||
|
|
||||||
|
final albumMap = Map<String, RemoteAlbum>.fromEntries(albums.map((a) => MapEntry(a.id, a)));
|
||||||
|
|
||||||
|
final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType<RemoteAlbum>().toList();
|
||||||
|
|
||||||
|
if (sortedAlbums.length < albums.length) {
|
||||||
|
final returnedIdSet = sortedIds.toSet();
|
||||||
|
final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id));
|
||||||
|
sortedAlbums.addAll(emptyAlbums);
|
||||||
}
|
}
|
||||||
|
|
||||||
// await all database queries
|
return sortedAlbums;
|
||||||
final entries = await Future.wait(
|
|
||||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
|
||||||
);
|
|
||||||
final assetTimestamps = Map.fromEntries(entries);
|
|
||||||
|
|
||||||
final sorted = albums.sorted((a, b) {
|
|
||||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return aDate.compareTo(bDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
|
|
||||||
// map album IDs to their oldest asset dates
|
|
||||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {
|
|
||||||
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
|
|
||||||
};
|
|
||||||
|
|
||||||
// await all database queries
|
|
||||||
final entries = await Future.wait(
|
|
||||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
|
||||||
);
|
|
||||||
final assetTimestamps = Map.fromEntries(entries);
|
|
||||||
|
|
||||||
final sorted = albums.sorted((a, b) {
|
|
||||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return aDate.compareTo(bDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
@@ -321,26 +323,32 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
Future<List<String>> getSortedAlbumIds(List<String> albumIds, {required AssetDateAggregation aggregation}) async {
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
if (albumIds.isEmpty) return [];
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
|
||||||
..join([
|
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
final jsonIds = jsonEncode(albumIds);
|
||||||
}
|
final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX';
|
||||||
|
|
||||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
final rows = await _db
|
||||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
.customSelect(
|
||||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
'''
|
||||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
SELECT
|
||||||
..join([
|
raae.album_id,
|
||||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
$sqlAgg(rae.local_date_time) 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
|
||||||
|
GROUP BY raae.album_id
|
||||||
|
ORDER BY asset_date ASC
|
||||||
|
''',
|
||||||
|
variables: [Variable<String>(jsonIds)],
|
||||||
|
readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity},
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
return rows.map((row) => row.read<String>('album_id')).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
@@ -13,38 +14,6 @@ void main() {
|
|||||||
late DriftRemoteAlbumRepository mockRemoteAlbumRepo;
|
late DriftRemoteAlbumRepository mockRemoteAlbumRepo;
|
||||||
late DriftAlbumApiRepository mockAlbumApiRepo;
|
late DriftAlbumApiRepository mockAlbumApiRepo;
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
mockRemoteAlbumRepo = MockRemoteAlbumRepository();
|
|
||||||
mockAlbumApiRepo = MockDriftAlbumApiRepository();
|
|
||||||
sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo);
|
|
||||||
|
|
||||||
when(() => mockRemoteAlbumRepo.getNewestAssetTimestamp(any())).thenAnswer((invocation) {
|
|
||||||
// Simulate a timestamp for the newest asset in the album
|
|
||||||
final albumID = invocation.positionalArguments[0] as String;
|
|
||||||
|
|
||||||
if (albumID == '1') {
|
|
||||||
return Future.value(DateTime(2023, 1, 1));
|
|
||||||
} else if (albumID == '2') {
|
|
||||||
return Future.value(DateTime(2023, 2, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
when(() => mockRemoteAlbumRepo.getOldestAssetTimestamp(any())).thenAnswer((invocation) {
|
|
||||||
// Simulate a timestamp for the oldest asset in the album
|
|
||||||
final albumID = invocation.positionalArguments[0] as String;
|
|
||||||
|
|
||||||
if (albumID == '1') {
|
|
||||||
return Future.value(DateTime(2019, 1, 1));
|
|
||||||
} else if (albumID == '2') {
|
|
||||||
return Future.value(DateTime(2019, 2, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
final albumA = RemoteAlbum(
|
final albumA = RemoteAlbum(
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Album A',
|
name: 'Album A',
|
||||||
@@ -73,6 +42,21 @@ void main() {
|
|||||||
isShared: false,
|
isShared: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockRemoteAlbumRepo = MockRemoteAlbumRepository();
|
||||||
|
mockAlbumApiRepo = MockDriftAlbumApiRepository();
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end),
|
||||||
|
).thenAnswer((_) async => ['1', '2']);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start),
|
||||||
|
).thenAnswer((_) async => ['1', '2']);
|
||||||
|
|
||||||
|
sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo);
|
||||||
|
});
|
||||||
|
|
||||||
group('sortAlbums', () {
|
group('sortAlbums', () {
|
||||||
test('should sort correctly based on name', () async {
|
test('should sort correctly based on name', () async {
|
||||||
final albums = [albumB, albumA];
|
final albums = [albumB, albumA];
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_album_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/infrastructure/repositories/remote_album.repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late Drift db;
|
||||||
|
late DriftRemoteAlbumRepository repository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||||
|
repository = DriftRemoteAlbumRepository(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getSortedAlbumIds', () {
|
||||||
|
Future<void> createUser(String userId, String name) async {
|
||||||
|
await db
|
||||||
|
.into(db.userEntity)
|
||||||
|
.insert(
|
||||||
|
UserEntityCompanion(
|
||||||
|
id: Value(userId),
|
||||||
|
name: Value(name),
|
||||||
|
email: Value('$userId@test.com'),
|
||||||
|
avatarColor: const Value(AvatarColor.primary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createAlbum(String albumId, String ownerId, String name) async {
|
||||||
|
await db
|
||||||
|
.into(db.remoteAlbumEntity)
|
||||||
|
.insert(
|
||||||
|
RemoteAlbumEntityCompanion(
|
||||||
|
id: Value(albumId),
|
||||||
|
name: Value(name),
|
||||||
|
ownerId: Value(ownerId),
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
description: const Value(''),
|
||||||
|
isActivityEnabled: const Value(false),
|
||||||
|
order: const Value(AlbumAssetOrder.asc),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createAsset(String assetId, String ownerId, DateTime createdAt) async {
|
||||||
|
await db
|
||||||
|
.into(db.remoteAssetEntity)
|
||||||
|
.insert(
|
||||||
|
RemoteAssetEntityCompanion(
|
||||||
|
id: Value(assetId),
|
||||||
|
checksum: Value('checksum-$assetId'),
|
||||||
|
name: Value('asset-$assetId'),
|
||||||
|
ownerId: Value(ownerId),
|
||||||
|
type: const Value(AssetType.image),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
updatedAt: Value(createdAt),
|
||||||
|
localDateTime: Value(createdAt),
|
||||||
|
durationInSeconds: const Value(0),
|
||||||
|
height: const Value(1080),
|
||||||
|
width: const Value(1920),
|
||||||
|
visibility: const Value(AssetVisibility.timeline),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> linkAssetToAlbum(String albumId, String assetId) async {
|
||||||
|
await db
|
||||||
|
.into(db.remoteAlbumAssetEntity)
|
||||||
|
.insert(RemoteAlbumAssetEntityCompanion(albumId: Value(albumId), assetId: Value(assetId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('returns empty list when albumIds is empty', () async {
|
||||||
|
final result = await repository.getSortedAlbumIds([], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns single album when only one album exists', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
const albumId = 'album1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
await createAlbum(albumId, userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 1));
|
||||||
|
await linkAssetToAlbum(albumId, 'asset1');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds([albumId], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
expect(result, [albumId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sorts albums by start date (MIN) ascending', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
// Album 1: Assets from Jan 10 to Jan 20 (start: Jan 10)
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 10));
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 20));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
await linkAssetToAlbum('album1', 'asset2');
|
||||||
|
|
||||||
|
// Album 2: Assets from Jan 5 to Jan 15 (start: Jan 5)
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset3', userId, DateTime(2024, 1, 5));
|
||||||
|
await createAsset('asset4', userId, DateTime(2024, 1, 15));
|
||||||
|
await linkAssetToAlbum('album2', 'asset3');
|
||||||
|
await linkAssetToAlbum('album2', 'asset4');
|
||||||
|
|
||||||
|
// Album 3: Assets from Jan 25 to Jan 30 (start: Jan 25)
|
||||||
|
await createAlbum('album3', userId, 'Album 3');
|
||||||
|
await createAsset('asset5', userId, DateTime(2024, 1, 25));
|
||||||
|
await createAsset('asset6', userId, DateTime(2024, 1, 30));
|
||||||
|
await linkAssetToAlbum('album3', 'asset5');
|
||||||
|
await linkAssetToAlbum('album3', 'asset6');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds([
|
||||||
|
'album1',
|
||||||
|
'album2',
|
||||||
|
'album3',
|
||||||
|
], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
// Expected order: album2 (Jan 5), album1 (Jan 10), album3 (Jan 25)
|
||||||
|
expect(result, ['album2', 'album1', 'album3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sorts albums by end date (MAX) ascending', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
// Album 1: Assets from Jan 10 to Jan 20 (end: Jan 20)
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 10));
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 20));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
await linkAssetToAlbum('album1', 'asset2');
|
||||||
|
|
||||||
|
// Album 2: Assets from Jan 5 to Jan 15 (end: Jan 15)
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset3', userId, DateTime(2024, 1, 5));
|
||||||
|
await createAsset('asset4', userId, DateTime(2024, 1, 15));
|
||||||
|
await linkAssetToAlbum('album2', 'asset3');
|
||||||
|
await linkAssetToAlbum('album2', 'asset4');
|
||||||
|
|
||||||
|
// Album 3: Assets from Jan 25 to Jan 30 (end: Jan 30)
|
||||||
|
await createAlbum('album3', userId, 'Album 3');
|
||||||
|
await createAsset('asset5', userId, DateTime(2024, 1, 25));
|
||||||
|
await createAsset('asset6', userId, DateTime(2024, 1, 30));
|
||||||
|
await linkAssetToAlbum('album3', 'asset5');
|
||||||
|
await linkAssetToAlbum('album3', 'asset6');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds([
|
||||||
|
'album1',
|
||||||
|
'album2',
|
||||||
|
'album3',
|
||||||
|
], aggregation: AssetDateAggregation.end);
|
||||||
|
|
||||||
|
// Expected order: album2 (Jan 15), album1 (Jan 20), album3 (Jan 30)
|
||||||
|
expect(result, ['album2', 'album1', 'album3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles albums with single asset', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 15));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 10));
|
||||||
|
await linkAssetToAlbum('album2', 'asset2');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds(['album1', 'album2'], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
expect(result, ['album2', 'album1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only returns requested album IDs in the result', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
// Create 3 albums
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 10));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 5));
|
||||||
|
await linkAssetToAlbum('album2', 'asset2');
|
||||||
|
|
||||||
|
await createAlbum('album3', userId, 'Album 3');
|
||||||
|
await createAsset('asset3', userId, DateTime(2024, 1, 15));
|
||||||
|
await linkAssetToAlbum('album3', 'asset3');
|
||||||
|
|
||||||
|
// Only request album1 and album3
|
||||||
|
final result = await repository.getSortedAlbumIds(['album1', 'album3'], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
// Should only return album1 and album3, not album2
|
||||||
|
expect(result, ['album1', 'album3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles albums with same date correctly', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
final sameDate = DateTime(2024, 1, 10);
|
||||||
|
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, sameDate);
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset2', userId, sameDate);
|
||||||
|
await linkAssetToAlbum('album2', 'asset2');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds(['album1', 'album2'], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
// Both albums have the same date, so both should be returned
|
||||||
|
expect(result, hasLength(2));
|
||||||
|
expect(result, containsAll(['album1', 'album2']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles albums across different years', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
await createAsset('asset1', userId, DateTime(2023, 12, 25));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 5));
|
||||||
|
await linkAssetToAlbum('album2', 'asset2');
|
||||||
|
|
||||||
|
await createAlbum('album3', userId, 'Album 3');
|
||||||
|
await createAsset('asset3', userId, DateTime(2025, 1, 1));
|
||||||
|
await linkAssetToAlbum('album3', 'asset3');
|
||||||
|
|
||||||
|
final result = await repository.getSortedAlbumIds([
|
||||||
|
'album1',
|
||||||
|
'album2',
|
||||||
|
'album3',
|
||||||
|
], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
expect(result, ['album1', 'album2', 'album3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles album with multiple assets correctly', () async {
|
||||||
|
const userId = 'user1';
|
||||||
|
|
||||||
|
await createUser(userId, 'Test User');
|
||||||
|
|
||||||
|
await createAlbum('album1', userId, 'Album 1');
|
||||||
|
// Album 1 has 5 assets from Jan 5 to Jan 25
|
||||||
|
await createAsset('asset1', userId, DateTime(2024, 1, 5));
|
||||||
|
await createAsset('asset2', userId, DateTime(2024, 1, 10));
|
||||||
|
await createAsset('asset3', userId, DateTime(2024, 1, 15));
|
||||||
|
await createAsset('asset4', userId, DateTime(2024, 1, 20));
|
||||||
|
await createAsset('asset5', userId, DateTime(2024, 1, 25));
|
||||||
|
await linkAssetToAlbum('album1', 'asset1');
|
||||||
|
await linkAssetToAlbum('album1', 'asset2');
|
||||||
|
await linkAssetToAlbum('album1', 'asset3');
|
||||||
|
await linkAssetToAlbum('album1', 'asset4');
|
||||||
|
await linkAssetToAlbum('album1', 'asset5');
|
||||||
|
|
||||||
|
await createAlbum('album2', userId, 'Album 2');
|
||||||
|
await createAsset('asset6', userId, DateTime(2024, 1, 1));
|
||||||
|
await linkAssetToAlbum('album2', 'asset6');
|
||||||
|
|
||||||
|
final resultStart = await repository.getSortedAlbumIds([
|
||||||
|
'album1',
|
||||||
|
'album2',
|
||||||
|
], aggregation: AssetDateAggregation.start);
|
||||||
|
|
||||||
|
// album2 (Jan 1) should come before album1 (Jan 5)
|
||||||
|
expect(resultStart, ['album2', 'album1']);
|
||||||
|
|
||||||
|
final resultEnd = await repository.getSortedAlbumIds(['album1', 'album2'], aggregation: AssetDateAggregation.end);
|
||||||
|
|
||||||
|
// album2 (Jan 1) should come before album1 (Jan 25)
|
||||||
|
expect(resultEnd, ['album2', 'album1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user