mirror of
https://github.com/immich-app/immich.git
synced 2026-07-05 20:57:40 +00:00
fix(mobile): stop sync albums crashing on the main isolate (#29133)
the album sync provider read cancellationProvider, which only exists in the background isolate and throws on the main one. moved the cancel signal onto the isolate call path. fixes #29125 fixes #29119
This commit is contained in:
parent
010220d588
commit
9ee412110f
3 changed files with 52 additions and 9 deletions
|
|
@ -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/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_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/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/providers/infrastructure/store.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
@ -19,7 +18,6 @@ final syncLinkedAlbumServiceProvider = Provider(
|
||||||
ref.watch(remoteAlbumRepository),
|
ref.watch(remoteAlbumRepository),
|
||||||
ref.watch(driftAlbumApiRepositoryProvider),
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
ref.watch(storeServiceProvider),
|
ref.watch(storeServiceProvider),
|
||||||
cancellation: ref.watch(cancellationProvider),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -28,19 +26,17 @@ class SyncLinkedAlbumService {
|
||||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
final StoreService _storeService;
|
final StoreService _storeService;
|
||||||
final Completer<void>? _cancellation;
|
|
||||||
|
|
||||||
SyncLinkedAlbumService(
|
SyncLinkedAlbumService(
|
||||||
this._localAlbumRepository,
|
this._localAlbumRepository,
|
||||||
this._remoteAlbumRepository,
|
this._remoteAlbumRepository,
|
||||||
this._albumApiRepository,
|
this._albumApiRepository,
|
||||||
this._storeService, {
|
this._storeService,
|
||||||
this._cancellation,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
final _log = Logger("SyncLinkedAlbumService");
|
final _log = Logger("SyncLinkedAlbumService");
|
||||||
|
|
||||||
Future<void> syncLinkedAlbums(String userId) async {
|
Future<void> syncLinkedAlbums(String userId, {Completer<void>? cancellation}) async {
|
||||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||||
|
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
|
|
@ -64,7 +60,7 @@ class SyncLinkedAlbumService {
|
||||||
final album = await _albumApiRepository.addAssets(
|
final album = await _albumApiRepository.addAssets(
|
||||||
remoteAlbum.id,
|
remoteAlbum.id,
|
||||||
assetIds,
|
assetIds,
|
||||||
abortTrigger: _cancellation?.future,
|
abortTrigger: cancellation?.future,
|
||||||
);
|
);
|
||||||
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||||
|
|
@ -10,5 +11,7 @@ Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||||
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
|
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
return ref
|
||||||
|
.read(syncLinkedAlbumServiceProvider)
|
||||||
|
.syncLinkedAlbums(user.id, cancellation: ref.read(cancellationProvider));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<SyncLinkedAlbumService>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manageLinkedAlbums runs from the UI without a cancellation signal', () {
|
||||||
|
final service = rootContainer().read(syncLinkedAlbumServiceProvider);
|
||||||
|
|
||||||
|
expect(service.manageLinkedAlbums(const [], 'user-1'), completes);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue