diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart index ddcd6721d7..091ca222f0 100644 --- a/mobile/lib/domain/services/sync_linked_album.service.dart +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -7,7 +7,6 @@ import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -19,7 +18,6 @@ final syncLinkedAlbumServiceProvider = Provider( ref.watch(remoteAlbumRepository), ref.watch(driftAlbumApiRepositoryProvider), ref.watch(storeServiceProvider), - cancellation: ref.watch(cancellationProvider), ), ); @@ -28,19 +26,17 @@ class SyncLinkedAlbumService { final DriftRemoteAlbumRepository _remoteAlbumRepository; final DriftAlbumApiRepository _albumApiRepository; final StoreService _storeService; - final Completer? _cancellation; SyncLinkedAlbumService( this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository, - this._storeService, { - this._cancellation, - }); + this._storeService, + ); final _log = Logger("SyncLinkedAlbumService"); - Future syncLinkedAlbums(String userId) async { + Future syncLinkedAlbums(String userId, {Completer? cancellation}) async { final selectedAlbums = await _localAlbumRepository.getBackupAlbums(); await Future.wait( @@ -64,7 +60,7 @@ class SyncLinkedAlbumService { final album = await _albumApiRepository.addAssets( remoteAlbum.id, assetIds, - abortTrigger: _cancellation?.future, + abortTrigger: cancellation?.future, ); await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); } diff --git a/mobile/lib/domain/utils/sync_linked_album.dart b/mobile/lib/domain/utils/sync_linked_album.dart index 7bfadc96e7..bdf35d3e2a 100644 --- a/mobile/lib/domain/utils/sync_linked_album.dart +++ b/mobile/lib/domain/utils/sync_linked_album.dart @@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:logging/logging.dart'; Future syncLinkedAlbumsIsolated(ProviderContainer ref) { @@ -10,5 +11,7 @@ Future syncLinkedAlbumsIsolated(ProviderContainer ref) { Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync"); return Future.value(); } - return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id); + return ref + .read(syncLinkedAlbumServiceProvider) + .syncLinkedAlbums(user.id, cancellation: ref.read(cancellationProvider)); } diff --git a/mobile/test/domain/services/sync_linked_album_service_test.dart b/mobile/test/domain/services/sync_linked_album_service_test.dart new file mode 100644 index 0000000000..d4fc1396c5 --- /dev/null +++ b/mobile/test/domain/services/sync_linked_album_service_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; + +import '../../infrastructure/repository.mock.dart'; +import '../service.mock.dart'; + +void main() { + // A container with the service's deps overridden but cancellationProvider left + // alone, i.e. the root (main) isolate, where cancellationProvider has no + // override and throws if read. The UI reads this provider here. + ProviderContainer rootContainer() { + final container = ProviderContainer( + overrides: [ + localAlbumRepository.overrideWithValue(MockLocalAlbumRepository()), + remoteAlbumRepository.overrideWithValue(MockRemoteAlbumRepository()), + driftAlbumApiRepositoryProvider.overrideWithValue(MockDriftAlbumApiRepository()), + storeServiceProvider.overrideWithValue(MockStoreService()), + ], + ); + addTearDown(container.dispose); + return container; + } + + // Regression for #29125 (Sync Albums toggle) and #29119 (can't leave the album + // selection screen): #28694 made the provider watch cancellationProvider, so + // reading it off the isolate threw. The cancellation now lives on the isolate + // call path, not the provider, so the UI can build it. + test('builds on the root isolate without a cancellationProvider override', () { + final container = rootContainer(); + + expect(() => container.read(syncLinkedAlbumServiceProvider), returnsNormally); + expect(container.read(syncLinkedAlbumServiceProvider), isA()); + }); + + test('manageLinkedAlbums runs from the UI without a cancellation signal', () { + final service = rootContainer().read(syncLinkedAlbumServiceProvider); + + expect(service.manageLinkedAlbums(const [], 'user-1'), completes); + }); +}