fix(mobile): newest/oldest album sort (#20743)

* fix(mobile): newest/oldest album sort

* chore: use sqlite to determine album asset timestamps

* Fix missing future

Co-authored-by: Alex <alex.tran1502@gmail.com>

* fix: async handling of sort

* chore: tests

* chore: code review changes

* fix: use created at for newest asset

* fix: use localDateTime for sorting

* chore: cleanup

* chore: use final

* feat: loading indicator

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Brandon Wees
2025-08-12 14:46:50 -05:00
committed by GitHub
parent 54960157c0
commit 0d60199514
8 changed files with 240 additions and 74 deletions

View File

@@ -0,0 +1,116 @@
import 'package:flutter_test/flutter_test.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/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
void main() {
late RemoteAlbumService sut;
late DriftRemoteAlbumRepository mockRemoteAlbumRepo;
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(
id: '1',
name: 'Album A',
description: "",
isActivityEnabled: false,
order: AlbumAssetOrder.asc,
assetCount: 1,
createdAt: DateTime(2023, 1, 1),
updatedAt: DateTime(2023, 1, 2),
ownerId: 'owner1',
ownerName: "Test User",
);
final albumB = RemoteAlbum(
id: '2',
name: 'Album B',
description: "",
isActivityEnabled: false,
order: AlbumAssetOrder.desc,
assetCount: 2,
createdAt: DateTime(2023, 2, 1),
updatedAt: DateTime(2023, 2, 2),
ownerId: 'owner2',
ownerName: "Test User",
);
group('sortAlbums', () {
test('should sort correctly based on name', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.title);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on createdAt', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.created);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on updatedAt', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.lastModified);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on assetCount', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.assetCount);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on newestAssetTimestamp', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostRecent);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on oldestAssetTimestamp', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostOldest);
expect(result, [albumB, albumA]);
});
});
}

View File

@@ -2,12 +2,14 @@ import 'package:immich_mobile/infrastructure/repositories/device_asset.repositor
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IsarStoreRepository {}
@@ -22,6 +24,8 @@ class MockSyncStreamRepository extends Mock implements SyncStreamRepository {}
class MockLocalAlbumRepository extends Mock implements DriftLocalAlbumRepository {}
class MockRemoteAlbumRepository extends Mock implements DriftRemoteAlbumRepository {}
class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository {}
class MockStorageRepository extends Mock implements StorageRepository {}
@@ -30,3 +34,5 @@ class MockStorageRepository extends Mock implements StorageRepository {}
class MockUserApiRepository extends Mock implements UserApiRepository {}
class MockSyncApiRepository extends Mock implements SyncApiRepository {}
class MockDriftAlbumApiRepository extends Mock implements DriftAlbumApiRepository {}