refactor: yeet old timeline (#27666)

* refactor: yank old timeline

# Conflicts:
#	mobile/lib/presentation/pages/editing/drift_edit.page.dart
#	mobile/lib/providers/websocket.provider.dart
#	mobile/lib/routing/router.dart

* more cleanup

* remove native code

* chore: bump sqlite-data version

* remove old background tasks from BGTaskSchedulerPermittedIdentifiers

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-04-15 23:00:27 +05:30
committed by GitHub
parent 6dd6053222
commit 79fccdbee0
367 changed files with 332 additions and 50870 deletions
-7
View File
@@ -1,20 +1,13 @@
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreService extends Mock implements StoreService {}
class MockUserService extends Mock implements UserService {}
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
class MockNativeSyncApi extends Mock implements NativeSyncApi {}
class MockAppSettingsService extends Mock implements AppSettingsService {}
class MockBackgroundUploadService extends Mock implements BackgroundUploadService {}
@@ -1,115 +0,0 @@
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/services/remote_album.service.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.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;
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",
isShared: false,
);
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",
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', () {
test('should sort correctly based on name', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.title);
expect(result, [albumA, albumB]);
});
test('should sort correctly based on createdAt', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.created);
expect(result, [albumB, albumA]);
});
test('should sort correctly based on updatedAt', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified);
expect(result, [albumB, albumA]);
});
test('should sort correctly based on assetCount', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount);
expect(result, [albumB, albumA]);
});
test('should sort correctly based on newestAssetTimestamp', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent);
expect(result, [albumB, albumA]);
});
test('should sort correctly based on oldestAssetTimestamp', () async {
final albums = [albumB, albumA];
final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest);
expect(result, [albumA, albumB]);
});
test('should flip order when isReverse is true for all modes', () async {
final albums = [albumB, albumA];
for (final mode in AlbumSortMode.values) {
final normal = await sut.sortAlbums(albums, mode, isReverse: false);
final reversed = await sut.sortAlbums(albums, mode, isReverse: true);
// reversed should be the exact inverse of normal
expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode');
}
});
});
}
@@ -1,185 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
import '../../test_utils.dart';
void main() {
late AssetService sut;
late MockRemoteAssetRepository mockRemoteAssetRepository;
late MockDriftLocalAssetRepository mockLocalAssetRepository;
setUp(() {
mockRemoteAssetRepository = MockRemoteAssetRepository();
mockLocalAssetRepository = MockDriftLocalAssetRepository();
sut = AssetService(
remoteAssetRepository: mockRemoteAssetRepository,
localAssetRepository: mockLocalAssetRepository,
);
});
group('getAspectRatio', () {
test('flips dimensions on Android for 90° and 270° orientations', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android');
}
});
test('does not flip dimensions on iOS regardless of orientation', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [0, 90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions');
}
});
test('fetches dimensions from remote repository when missing from asset', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
final exif = const ExifInfo(orientation: '1');
final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080);
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1920 / 1080);
verify(() => mockRemoteAssetRepository.get('remote-1')).called(1);
});
test('fetches dimensions from local repository when missing from local asset', () async {
final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0);
final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0);
when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080);
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
});
test('uses fetched asset orientation when dimensions are missing on Android', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
// Original asset has default orientation 0, but dimensions are missing
final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0);
// Fetched asset has 90° orientation and proper dimensions
final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90);
when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset);
final result = await sut.getAspectRatio(localAsset);
// Should flip dimensions since fetched asset has 90° orientation
expect(result, 1080 / 1920);
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
});
test('returns 1.0 when dimensions are still unavailable after fetching', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
final exif = const ExifInfo(orientation: '1');
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1.0);
});
test('returns 1.0 when height is zero', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0);
final exif = const ExifInfo(orientation: '1');
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1.0);
});
test('handles local asset with remoteId using local orientation not remote exif', () async {
// When a LocalAsset has a remoteId (merged), we should use local orientation
// because the width/height come from the local asset (pre-corrected on iOS)
final localAsset = TestUtils.createLocalAsset(
id: 'local-1',
remoteId: 'remote-1',
width: 1920,
height: 1080,
orientation: 0,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080);
// Should not call remote exif for LocalAsset
verifyNever(() => mockRemoteAssetRepository.getExif(any()));
});
test('handles local asset with remoteId and 90 degree rotation on Android', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
final localAsset = TestUtils.createLocalAsset(
id: 'local-1',
remoteId: 'remote-1',
width: 1920,
height: 1080,
orientation: 90,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1080 / 1920);
});
test('should not flip remote asset dimensions', () async {
final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90'];
for (final orientation in flippedOrientations) {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
final exif = ExifInfo(orientation: orientation);
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation');
}
});
});
}
@@ -29,11 +29,11 @@ final _kWarnLog = LogMessage(
void main() {
late LogService sut;
late LogRepository mockLogRepo;
late IsarStoreRepository mockStoreRepo;
late DriftStoreRepository mockStoreRepo;
setUp(() async {
mockLogRepo = MockLogRepository();
mockStoreRepo = MockStoreRepository();
mockStoreRepo = MockDriftStoreRepository();
registerFallbackValue(_kInfoLog);
@@ -15,13 +15,11 @@ final _kBackupFailedSince = DateTime.utc(2023);
void main() {
late StoreService sut;
late IsarStoreRepository mockStoreRepo;
late DriftStoreRepository mockDriftStoreRepo;
late StreamController<List<StoreDto<Object>>> controller;
setUp(() async {
controller = StreamController<List<StoreDto<Object>>>.broadcast();
mockStoreRepo = MockStoreRepository();
mockDriftStoreRepo = MockDriftStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.accessToken);
@@ -29,16 +27,6 @@ void main() {
registerFallbackValue(StoreKey.backgroundBackup);
registerFallbackValue(StoreKey.backupFailedSince);
when(() => mockStoreRepo.getAll()).thenAnswer(
(_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken),
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
],
);
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken),
@@ -49,7 +37,7 @@ void main() {
);
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
sut = await StoreService.create(storeRepository: mockStoreRepo);
sut = await StoreService.create(storeRepository: mockDriftStoreRepo);
});
tearDown(() async {
@@ -59,7 +47,7 @@ void main() {
group("Store Service Init:", () {
test('Populates the internal cache on init', () {
verify(() => mockStoreRepo.getAll()).called(1);
verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy);
@@ -74,7 +62,7 @@ void main() {
await pumpEventQueue();
verify(() => mockStoreRepo.watchAll()).called(1);
verify(() => mockDriftStoreRepo.watchAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase());
});
});
@@ -95,19 +83,18 @@ void main() {
group('Store Service put:', () {
setUp(() {
when(() => mockStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
when(() => mockDriftStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
});
test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.accessToken, _kAccessToken);
verifyNever(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, any()));
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, any()));
});
test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.accessToken, newAccessToken);
verify(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
});
});
@@ -117,7 +104,6 @@ void main() {
setUp(() {
valueController = StreamController<String?>.broadcast();
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
when(() => mockDriftStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
});
@@ -136,19 +122,18 @@ void main() {
}
await pumpEventQueue();
verify(() => mockStoreRepo.watch<String>(StoreKey.accessToken)).called(1);
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.accessToken)).called(1);
});
});
group('Store Service delete:', () {
setUp(() {
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
when(() => mockDriftStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
});
test('Removes the value from the DB', () async {
await sut.delete(StoreKey.accessToken);
verify(() => mockStoreRepo.delete<String>(StoreKey.accessToken)).called(1);
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.accessToken)).called(1);
});
test('Removes the value from the cache', () async {
@@ -159,13 +144,12 @@ void main() {
group('Store Service clear:', () {
setUp(() {
when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true);
when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true);
});
test('Clears all values from the store', () async {
await sut.clear();
verify(() => mockStoreRepo.deleteAll()).called(1);
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), isNull);
expect(sut.tryGet(StoreKey.backgroundBackup), isNull);
expect(sut.tryGet(StoreKey.groupAssetsBy), isNull);
@@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:mocktail/mocktail.dart';
@@ -14,19 +13,13 @@ import '../service.mock.dart';
void main() {
late UserService sut;
late IsarUserRepository mockUserRepo;
late UserApiRepository mockUserApiRepo;
late StoreService mockStoreService;
setUp(() {
mockUserRepo = MockIsarUserRepository();
mockUserApiRepo = MockUserApiRepository();
mockStoreService = MockStoreService();
sut = UserService(
isarUserRepository: mockUserRepo,
userApiRepository: mockUserApiRepo,
storeService: mockStoreService,
);
sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService);
registerFallbackValue(UserStub.admin);
when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin);
@@ -77,11 +70,9 @@ void main() {
test('should return user from api and store it', () async {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true);
when(() => mockUserRepo.update(UserStub.admin)).thenAnswer((_) async => UserStub.admin);
final result = await sut.refreshMyUser();
verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1);
verify(() => mockUserRepo.update(UserStub.admin)).called(1);
expect(result, UserStub.admin);
});
@@ -90,7 +81,6 @@ void main() {
final result = await sut.refreshMyUser();
verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin));
verifyNever(() => mockUserRepo.update(UserStub.admin));
expect(result, isNull);
});
});
@@ -104,12 +94,10 @@ void main() {
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenAnswer((_) async => profileImagePath);
when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true);
when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin);
final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1);
verify(() => mockUserRepo.update(updatedUser)).called(1);
expect(result, profileImagePath);
});
@@ -123,7 +111,6 @@ void main() {
final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser));
verifyNever(() => mockUserRepo.update(updatedUser));
expect(result, isNull);
});
});