refactor(mobile): use user service methods (#16783)

* refactor: user entity

* chore: rebase fixes

* refactor(mobile): refactor to use user service methods

* fix: late init error

* fix: lint

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2025-03-18 19:02:33 +05:30 committed by GitHub
parent 6c2985df26
commit dd263b010c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 137 additions and 143 deletions

View File

@ -44,10 +44,14 @@ class UserService {
Future<String?> createProfileImage(String name, Uint8List image) async { Future<String?> createProfileImage(String name, Uint8List image) async {
try { try {
return await _userApiRepository.createProfileImage( final path = await _userApiRepository.createProfileImage(
name: name, name: name,
data: image, data: image,
); );
final updatedUser = getMyUser().copyWith(profileImagePath: path);
await _storeService.put(StoreKey.currentUser, updatedUser);
await _userRepository.update(updatedUser);
return path;
} catch (e) { } catch (e) {
_log.warning("Failed to upload profile image", e); _log.warning("Failed to upload profile image", e);
return null; return null;

View File

@ -3,11 +3,12 @@ import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
@ -18,20 +19,20 @@ final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier( return AuthNotifier(
ref.watch(authServiceProvider), ref.watch(authServiceProvider),
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(userServiceProvider),
); );
}); });
class AuthNotifier extends StateNotifier<AuthState> { class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService; final AuthService _authService;
final ApiService _apiService; final ApiService _apiService;
final UserService _userService;
final _log = Logger("AuthenticationNotifier"); final _log = Logger("AuthenticationNotifier");
static const Duration _timeoutDuration = Duration(seconds: 7); static const Duration _timeoutDuration = Duration(seconds: 7);
AuthNotifier( AuthNotifier(this._authService, this._apiService, this._userService)
this._authService, : super(
this._apiService,
) : super(
AuthState( AuthState(
deviceId: "", deviceId: "",
userId: "", userId: "",
@ -106,17 +107,21 @@ class AuthNotifier extends StateNotifier<AuthState> {
String deviceId = String deviceId =
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
UserDto? user = Store.tryGet(StoreKey.currentUser); UserDto? user = _userService.tryGetMyUser();
UserAdminResponseDto? userResponse;
UserPreferencesResponseDto? userPreferences;
try { try {
final responses = await Future.wait([ final serverUser =
_apiService.usersApi.getMyUser().timeout(_timeoutDuration), await _userService.refreshMyUser().timeout(_timeoutDuration);
_apiService.usersApi.getMyPreferences().timeout(_timeoutDuration), if (serverUser == null) {
]); _log.severe("Unable to get user information from the server.");
userResponse = responses[0] as UserAdminResponseDto; } else {
userPreferences = responses[1] as UserPreferencesResponseDto; // If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
user = serverUser;
await Store.put(StoreKey.deviceId, deviceId);
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
await Store.put(StoreKey.accessToken, accessToken);
}
} on ApiException catch (error, stackTrace) { } on ApiException catch (error, stackTrace) {
if (error.code == 401) { if (error.code == 401) {
_log.severe("Unauthorized access, token likely expired. Logging out."); _log.severe("Unauthorized access, token likely expired. Logging out.");
@ -140,22 +145,6 @@ class AuthNotifier extends StateNotifier<AuthState> {
} }
} }
// If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
if (userResponse == null) {
_log.severe("Unable to get user information from the server.");
} else {
await Store.put(StoreKey.deviceId, deviceId);
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
await Store.put(
StoreKey.currentUser,
UserConverter.fromAdminDto(userResponse, userPreferences),
);
await Store.put(StoreKey.accessToken, accessToken);
user = UserConverter.fromAdminDto(userResponse, userPreferences);
}
// If the user is null, the login was not successful // If the user is null, the login was not successful
// and we don't have a local copy of the user from a prior successful login // and we don't have a local copy of the user from a prior successful login
if (user == null) { if (user == null) {
@ -163,13 +152,13 @@ class AuthNotifier extends StateNotifier<AuthState> {
} }
state = state.copyWith( state = state.copyWith(
isAuthenticated: true, deviceId: deviceId,
userId: user.uid, userId: user.uid,
userEmail: user.email, userEmail: user.email,
isAuthenticated: true,
name: user.name, name: user.name,
profileImagePath: user.profileImagePath,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
deviceId: deviceId, profileImagePath: user.profileImagePath,
); );
return true; return true;

View File

@ -1,34 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/timeline.service.dart';
class CurrentUserProvider extends StateNotifier<UserDto?> { class CurrentUserProvider extends StateNotifier<UserDto?> {
CurrentUserProvider(this._apiService) : super(null) { CurrentUserProvider(this._userService) : super(null) {
state = Store.tryGet(StoreKey.currentUser); state = _userService.tryGetMyUser();
streamSub = streamSub =
Store.watch(StoreKey.currentUser).listen((user) => state = user); _userService.watchMyUser().listen((user) => state = user ?? state);
} }
final ApiService _apiService; final UserService _userService;
late final StreamSubscription<UserDto?> streamSub; late final StreamSubscription<UserDto?> streamSub;
refresh() async { refresh() async {
try { try {
final user = await _apiService.usersApi.getMyUser(); await _userService.refreshMyUser();
final userPreferences = await _apiService.usersApi.getMyPreferences();
if (user != null) {
await Store.put(
StoreKey.currentUser,
UserConverter.fromAdminDto(user, userPreferences),
);
}
} catch (_) {} } catch (_) {}
} }
@ -41,9 +31,7 @@ class CurrentUserProvider extends StateNotifier<UserDto?> {
final currentUserProvider = final currentUserProvider =
StateNotifierProvider<CurrentUserProvider, UserDto?>((ref) { StateNotifierProvider<CurrentUserProvider, UserDto?>((ref) {
return CurrentUserProvider( return CurrentUserProvider(ref.watch(userServiceProvider));
ref.watch(apiServiceProvider),
);
}); });
class TimelineUserIdsProvider extends StateNotifier<List<int>> { class TimelineUserIdsProvider extends StateNotifier<List<int>> {

View File

@ -1,11 +1,8 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@ -28,19 +25,7 @@ class TabNavigationObserver extends AutoRouterObserver {
// Update user info // Update user info
try { try {
final userResponseDto = ref.read(userServiceProvider).refreshMyUser();
await ref.read(apiServiceProvider).usersApi.getMyUser();
final userPreferences =
await ref.read(apiServiceProvider).usersApi.getMyPreferences();
if (userResponseDto == null) {
return;
}
await Store.put(
StoreKey.currentUser,
UserConverter.fromAdminDto(userResponseDto, userPreferences),
);
ref.read(serverInfoProvider.notifier).getServerVersion(); ref.read(serverInfoProvider.notifier).getServerVersion();
} catch (e) { } catch (e) {
debugPrint("Error refreshing user info $e"); debugPrint("Error refreshing user info $e");

View File

@ -6,12 +6,11 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity; as entity;
import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album.interface.dart';
@ -21,6 +20,7 @@ import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart';
@ -33,6 +33,7 @@ import 'package:logging/logging.dart';
final albumServiceProvider = Provider( final albumServiceProvider = Provider(
(ref) => AlbumService( (ref) => AlbumService(
ref.watch(syncServiceProvider), ref.watch(syncServiceProvider),
ref.watch(userServiceProvider),
ref.watch(entityServiceProvider), ref.watch(entityServiceProvider),
ref.watch(albumRepositoryProvider), ref.watch(albumRepositoryProvider),
ref.watch(assetRepositoryProvider), ref.watch(assetRepositoryProvider),
@ -44,6 +45,7 @@ final albumServiceProvider = Provider(
class AlbumService { class AlbumService {
final SyncService _syncService; final SyncService _syncService;
final UserService _userService;
final EntityService _entityService; final EntityService _entityService;
final IAlbumRepository _albumRepository; final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository; final IAssetRepository _assetRepository;
@ -56,6 +58,7 @@ class AlbumService {
AlbumService( AlbumService(
this._syncService, this._syncService,
this._userService,
this._entityService, this._entityService,
this._albumRepository, this._albumRepository,
this._assetRepository, this._assetRepository,
@ -292,7 +295,7 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async { Future<bool> deleteAlbum(Album album) async {
try { try {
final userId = Store.get(StoreKey.currentUser).id; final userId = _userService.getMyUser().id;
if (album.owner.value?.isarId == userId) { if (album.owner.value?.isarId == userId) {
await _albumApiRepository.delete(album.remoteId!); await _albumApiRepository.delete(album.remoteId!);
} }

View File

@ -35,6 +35,9 @@ class ApiService implements Authentication {
late MemoriesApi memoriesApi; late MemoriesApi memoriesApi;
ApiService() { ApiService() {
// The below line ensures that the api clients are initialized when the service is instantiated
// This is required to avoid late initialization errors when the clients are access before the endpoint is resolved
setEndpoint('');
final endpoint = Store.tryGet(StoreKey.serverEndpoint); final endpoint = Store.tryGet(StoreKey.serverEndpoint);
if (endpoint != null && endpoint.isNotEmpty) { if (endpoint != null && endpoint.isNotEmpty) {
setEndpoint(endpoint); setEndpoint(endpoint);

View File

@ -6,9 +6,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
@ -19,9 +18,7 @@ import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'
hide userServiceProvider;
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
@ -47,7 +44,7 @@ final assetServiceProvider = Provider(
ref.watch(syncServiceProvider), ref.watch(syncServiceProvider),
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),
ref.watch(albumServiceProvider), ref.watch(albumServiceProvider),
ref.watch(storeServiceProvider), ref.watch(userServiceProvider),
ref.watch(assetMediaRepositoryProvider), ref.watch(assetMediaRepositoryProvider),
), ),
); );
@ -63,7 +60,7 @@ class AssetService {
final SyncService _syncService; final SyncService _syncService;
final BackupService _backupService; final BackupService _backupService;
final AlbumService _albumService; final AlbumService _albumService;
final StoreService _storeService; final UserService _userService;
final IAssetMediaRepository _assetMediaRepository; final IAssetMediaRepository _assetMediaRepository;
final log = Logger('AssetService'); final log = Logger('AssetService');
@ -78,7 +75,7 @@ class AssetService {
this._syncService, this._syncService,
this._backupService, this._backupService,
this._albumService, this._albumService,
this._storeService, this._userService,
this._assetMediaRepository, this._assetMediaRepository,
); );
@ -316,7 +313,7 @@ class AssetService {
); );
await refreshRemoteAssets(); await refreshRemoteAssets();
final owner = _storeService.get(StoreKey.currentUser); final owner = _userService.getMyUser();
final remoteAssets = await _assetRepository.getAll( final remoteAssets = await _assetRepository.getAll(
ownerId: owner.id, ownerId: owner.id,
state: AssetState.merged, state: AssetState.merged,
@ -522,12 +519,12 @@ class AssetService {
} }
Future<List<Asset>> getRecentlyAddedAssets() { Future<List<Asset>> getRecentlyAddedAssets() {
final me = _storeService.get(StoreKey.currentUser); final me = _userService.getMyUser();
return _assetRepository.getRecentlyAddedAssets(me.id); return _assetRepository.getRecentlyAddedAssets(me.id);
} }
Future<List<Asset>> getMotionAssets() { Future<List<Asset>> getMotionAssets() {
final me = _storeService.get(StoreKey.currentUser); final me = _userService.getMyUser();
return _assetRepository.getMotionAssets(me.id); return _assetRepository.getMotionAssets(me.id);
} }
} }

View File

@ -8,12 +8,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.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/user.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
@ -22,11 +24,13 @@ import 'package:immich_mobile/utils/diff.dart';
/// Finds duplicates originating from missing EXIF information /// Finds duplicates originating from missing EXIF information
class BackupVerificationService { class BackupVerificationService {
final UserService _userService;
final IFileMediaRepository _fileMediaRepository; final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository; final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository; final IExifInfoRepository _exifInfoRepository;
BackupVerificationService( const BackupVerificationService(
this._userService,
this._fileMediaRepository, this._fileMediaRepository,
this._assetRepository, this._assetRepository,
this._exifInfoRepository, this._exifInfoRepository,
@ -34,7 +38,7 @@ class BackupVerificationService {
/// Returns at most [limit] assets that were backed up without exif /// Returns at most [limit] assets that were backed up without exif
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async { Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
final owner = Store.get(StoreKey.currentUser).id; final owner = _userService.getMyUser().id;
final List<Asset> onlyLocal = await _assetRepository.getAll( final List<Asset> onlyLocal = await _assetRepository.getAll(
ownerId: owner, ownerId: owner,
state: AssetState.local, state: AssetState.local,
@ -214,6 +218,7 @@ class BackupVerificationService {
final backupVerificationServiceProvider = Provider( final backupVerificationServiceProvider = Provider(
(ref) => BackupVerificationService( (ref) => BackupVerificationService(
ref.watch(userServiceProvider),
ref.watch(fileMediaRepositoryProvider), ref.watch(fileMediaRepositoryProvider),
ref.watch(assetRepositoryProvider), ref.watch(assetRepositoryProvider),
ref.watch(exifRepositoryProvider), ref.watch(exifRepositoryProvider),

View File

@ -5,9 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart'; import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart';
@ -20,7 +19,6 @@ import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart';
@ -47,7 +45,7 @@ final syncServiceProvider = Provider(
ref.watch(exifRepositoryProvider), ref.watch(exifRepositoryProvider),
ref.watch(partnerRepositoryProvider), ref.watch(partnerRepositoryProvider),
ref.watch(userRepositoryProvider), ref.watch(userRepositoryProvider),
ref.watch(storeServiceProvider), ref.watch(userServiceProvider),
ref.watch(etagRepositoryProvider), ref.watch(etagRepositoryProvider),
ref.watch(partnerApiRepositoryProvider), ref.watch(partnerApiRepositoryProvider),
ref.watch(userApiRepositoryProvider), ref.watch(userApiRepositoryProvider),
@ -63,8 +61,8 @@ class SyncService {
final IAssetRepository _assetRepository; final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository; final IExifInfoRepository _exifInfoRepository;
final IUserRepository _userRepository; final IUserRepository _userRepository;
final UserService _userService;
final IPartnerRepository _partnerRepository; final IPartnerRepository _partnerRepository;
final StoreService _storeService;
final IETagRepository _eTagRepository; final IETagRepository _eTagRepository;
final IPartnerApiRepository _partnerApiRepository; final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository; final IUserApiRepository _userApiRepository;
@ -81,7 +79,7 @@ class SyncService {
this._exifInfoRepository, this._exifInfoRepository,
this._partnerRepository, this._partnerRepository,
this._userRepository, this._userRepository,
this._storeService, this._userService,
this._eTagRepository, this._eTagRepository,
this._partnerApiRepository, this._partnerApiRepository,
this._userApiRepository, this._userApiRepository,
@ -210,7 +208,7 @@ class SyncService {
DateTime since, DateTime since,
) getChangedAssets, ) getChangedAssets,
) async { ) async {
final currentUser = _storeService.get(StoreKey.currentUser); final currentUser = _userService.getMyUser();
final DateTime? since = final DateTime? since =
(await _eTagRepository.get(currentUser.id))?.time?.toUtc(); (await _eTagRepository.get(currentUser.id))?.time?.toUtc();
if (since == null) return null; if (since == null) return null;
@ -261,7 +259,7 @@ class SyncService {
Future<List<UserDto>> _getAllAccessibleUsers() async { Future<List<UserDto>> _getAllAccessibleUsers() async {
final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); final sharedWith = (await _partnerRepository.getSharedWith()).toSet();
sharedWith.add(_storeService.get(StoreKey.currentUser)); sharedWith.add(_userService.getMyUser());
return sharedWith.toList(); return sharedWith.toList();
} }
@ -455,7 +453,7 @@ class SyncService {
} }
if (album.shared || dto.shared) { if (album.shared || dto.shared) {
final userId = (_storeService.get(StoreKey.currentUser)).id; final userId = (_userService.getMyUser()).id;
final foreign = final foreign =
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
existing.addAll(foreign); existing.addAll(foreign);
@ -591,7 +589,7 @@ class SyncService {
// general case, e.g. some assets have been deleted or there are excluded albums on iOS // general case, e.g. some assets have been deleted or there are excluded albums on iOS
final inDb = await _assetRepository.getByAlbum( final inDb = await _assetRepository.getByAlbum(
dbAlbum, dbAlbum,
ownerId: (_storeService.get(StoreKey.currentUser)).id, ownerId: (_userService.getMyUser()).id,
sortBy: AssetSort.checksum, sortBy: AssetSort.checksum,
); );

View File

@ -1,11 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart'; import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/timeline.repository.dart'; import 'package:immich_mobile/repositories/timeline.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
@ -14,28 +13,28 @@ final timelineServiceProvider = Provider<TimelineService>((ref) {
return TimelineService( return TimelineService(
ref.watch(timelineRepositoryProvider), ref.watch(timelineRepositoryProvider),
ref.watch(appSettingsServiceProvider), ref.watch(appSettingsServiceProvider),
ref.watch(storeServiceProvider), ref.watch(userServiceProvider),
); );
}); });
class TimelineService { class TimelineService {
final ITimelineRepository _timelineRepository; final ITimelineRepository _timelineRepository;
final AppSettingsService _appSettingsService; final AppSettingsService _appSettingsService;
final StoreService _storeService; final UserService _userService;
const TimelineService( const TimelineService(
this._timelineRepository, this._timelineRepository,
this._appSettingsService, this._appSettingsService,
this._storeService, this._userService,
); );
Future<List<int>> getTimelineUserIds() async { Future<List<int>> getTimelineUserIds() async {
final me = _storeService.get(StoreKey.currentUser); final me = _userService.getMyUser();
return _timelineRepository.getTimelineUserIds(me.id); return _timelineRepository.getTimelineUserIds(me.id);
} }
Stream<List<int>> watchTimelineUserIds() async* { Stream<List<int>> watchTimelineUserIds() async* {
final me = _storeService.get(StoreKey.currentUser); final me = _userService.getMyUser();
yield* _timelineRepository.watchTimelineUsers(me.id); yield* _timelineRepository.watchTimelineUsers(me.id);
} }
@ -51,13 +50,13 @@ class TimelineService {
} }
Stream<RenderList> watchArchiveTimeline() async* { Stream<RenderList> watchArchiveTimeline() async* {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
yield* _timelineRepository.watchArchiveTimeline(user.id); yield* _timelineRepository.watchArchiveTimeline(user.id);
} }
Stream<RenderList> watchFavoriteTimeline() async* { Stream<RenderList> watchFavoriteTimeline() async* {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
yield* _timelineRepository.watchFavoriteTimeline(user.id); yield* _timelineRepository.watchFavoriteTimeline(user.id);
} }
@ -70,7 +69,7 @@ class TimelineService {
} }
Stream<RenderList> watchTrashTimeline() async* { Stream<RenderList> watchTrashTimeline() async* {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
yield* _timelineRepository.watchTrashTimeline(user.id); yield* _timelineRepository.watchTrashTimeline(user.id);
} }
@ -97,7 +96,7 @@ class TimelineService {
} }
Stream<RenderList> watchAssetSelectionTimeline() async* { Stream<RenderList> watchAssetSelectionTimeline() async* {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
yield* _timelineRepository.watchAssetSelectionTimeline(user.id); yield* _timelineRepository.watchAssetSelectionTimeline(user.id);
} }

View File

@ -1,10 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -13,19 +12,19 @@ final trashServiceProvider = Provider<TrashService>((ref) {
return TrashService( return TrashService(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(assetRepositoryProvider), ref.watch(assetRepositoryProvider),
ref.watch(storeServiceProvider), ref.watch(userServiceProvider),
); );
}); });
class TrashService { class TrashService {
final ApiService _apiService; final ApiService _apiService;
final IAssetRepository _assetRepository; final IAssetRepository _assetRepository;
final StoreService _storeService; final UserService _userService;
TrashService( const TrashService(
this._apiService, this._apiService,
this._assetRepository, this._assetRepository,
this._storeService, this._userService,
); );
Future<void> restoreAssets(Iterable<Asset> assetList) async { Future<void> restoreAssets(Iterable<Asset> assetList) async {
@ -43,7 +42,7 @@ class TrashService {
} }
Future<void> emptyTrash() async { Future<void> emptyTrash() async {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
await _apiService.trashApi.emptyTrash(); await _apiService.trashApi.emptyTrash();
@ -74,7 +73,7 @@ class TrashService {
} }
Future<void> restoreTrash() async { Future<void> restoreTrash() async {
final user = _storeService.get(StoreKey.currentUser); final user = _userService.getMyUser();
await _apiService.trashApi.restoreTrash(); await _apiService.trashApi.restoreTrash();

View File

@ -1,13 +1,13 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
class AlbumThumbnailCard extends StatelessWidget { class AlbumThumbnailCard extends ConsumerWidget {
final Function()? onTap; final Function()? onTap;
/// Whether or not to show the owner of the album (or "Owned") /// Whether or not to show the owner of the album (or "Owned")
@ -26,7 +26,7 @@ class AlbumThumbnailCard extends StatelessWidget {
final Album album; final Album album;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
var cardSize = constraints.maxWidth; var cardSize = constraints.maxWidth;
@ -58,7 +58,7 @@ class AlbumThumbnailCard extends StatelessWidget {
// Add the owner name to the subtitle // Add the owner name to the subtitle
String? owner; String? owner;
if (showOwner) { if (showOwner) {
if (album.ownerId == Store.get(StoreKey.currentUser).uid) { if (album.ownerId == ref.read(currentUserProvider)?.uid) {
owner = 'album_thumbnail_owned'.tr(); owner = 'album_thumbnail_owned'.tr();
} else if (album.ownerName != null) { } else if (album.ownerName != null) {
owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]); owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]);

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
@ -21,7 +19,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
final authState = ref.watch(authProvider); final authState = ref.watch(authProvider);
final uploadProfileImageStatus = final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status; ref.watch(uploadProfileImageProvider).status;
final user = Store.tryGet(StoreKey.currentUser); final user = ref.watch(currentUserProvider);
buildUserProfileImage() { buildUserProfileImage() {
if (user == null) { if (user == null) {
@ -67,9 +65,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
profileImagePath, profileImagePath,
); );
if (user != null) { if (user != null) {
final updatedUser =
user.copyWith(profileImagePath: profileImagePath);
await Store.put(StoreKey.currentUser, updatedUser);
ref.read(currentUserProvider.notifier).refresh(); ref.read(currentUserProvider.notifier).refresh();
} }
} }

View File

@ -3,14 +3,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/immich_logo_provider.dart'; import 'package:immich_mobile/providers/immich_logo_provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@ -30,7 +29,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
backupState.backgroundBackup || backupState.autoBackup; backupState.backgroundBackup || backupState.autoBackup;
final ServerInfo serverInfoState = ref.watch(serverInfoProvider); final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final immichLogo = ref.watch(immichLogoProvider); final immichLogo = ref.watch(immichLogoProvider);
final user = Store.tryGet(StoreKey.currentUser); final user = ref.watch(currentUserProvider);
final isDarkTheme = context.isDarkTheme; final isDarkTheme = context.isDarkTheme;
const widgetSize = 30.0; const widgetSize = 30.0;

View File

@ -27,12 +27,16 @@ void main() {
userApiRepository: mockUserApiRepo, userApiRepository: mockUserApiRepo,
storeService: mockStoreService, storeService: mockStoreService,
); );
registerFallbackValue(UserStub.admin);
when(() => mockStoreService.get(StoreKey.currentUser))
.thenReturn(UserStub.admin);
when(() => mockStoreService.tryGet(StoreKey.currentUser))
.thenReturn(UserStub.admin);
}); });
group('getMyUser', () { group('getMyUser', () {
test('should return user from store', () { test('should return user from store', () {
when(() => mockStoreService.get(StoreKey.currentUser))
.thenReturn(UserStub.admin);
final result = sut.getMyUser(); final result = sut.getMyUser();
expect(result, UserStub.admin); expect(result, UserStub.admin);
}); });
@ -47,8 +51,6 @@ void main() {
group('tryGetMyUser', () { group('tryGetMyUser', () {
test('should return user from store', () { test('should return user from store', () {
when(() => mockStoreService.tryGet(StoreKey.currentUser))
.thenReturn(UserStub.admin);
final result = sut.tryGetMyUser(); final result = sut.tryGetMyUser();
expect(result, UserStub.admin); expect(result, UserStub.admin);
}); });
@ -107,26 +109,48 @@ void main() {
group('createProfileImage', () { group('createProfileImage', () {
test('should return profile image path', () async { test('should return profile image path', () async {
const profileImagePath = 'profile.jpg';
final updatedUser =
UserStub.admin.copyWith(profileImagePath: profileImagePath);
when( when(
() => mockUserApiRepo.createProfileImage( () => mockUserApiRepo.createProfileImage(
name: 'profile.jpg', name: profileImagePath,
data: Uint8List(0), data: Uint8List(0),
), ),
).thenAnswer((_) async => 'profile.jpg'); ).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('profile.jpg', Uint8List(0)); final result =
expect(result, 'profile.jpg'); await sut.createProfileImage(profileImagePath, Uint8List(0));
verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser))
.called(1);
verify(() => mockUserRepo.update(updatedUser)).called(1);
expect(result, profileImagePath);
}); });
test('should return null if profile image creation fails', () async { test('should return null if profile image creation fails', () async {
const profileImagePath = 'profile.jpg';
final updatedUser =
UserStub.admin.copyWith(profileImagePath: profileImagePath);
when( when(
() => mockUserApiRepo.createProfileImage( () => mockUserApiRepo.createProfileImage(
name: 'profile.jpg', name: profileImagePath,
data: Uint8List(0), data: Uint8List(0),
), ),
).thenThrow(Exception('Failed to create profile image')); ).thenThrow(Exception('Failed to create profile image'));
final result = await sut.createProfileImage('profile.jpg', Uint8List(0)); final result =
await sut.createProfileImage(profileImagePath, Uint8List(0));
verifyNever(
() => mockStoreService.put(StoreKey.currentUser, updatedUser),
);
verifyNever(() => mockUserRepo.update(updatedUser));
expect(result, isNull); expect(result, isNull);
}); });
}); });

View File

@ -15,6 +15,7 @@ import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import '../../domain/service.mock.dart';
import '../../infrastructure/repository.mock.dart'; import '../../infrastructure/repository.mock.dart';
import '../../repository.mocks.dart'; import '../../repository.mocks.dart';
import '../../service.mocks.dart'; import '../../service.mocks.dart';
@ -62,6 +63,7 @@ void main() {
MockPartnerApiRepository(); MockPartnerApiRepository();
final MockUserApiRepository userApiRepository = MockUserApiRepository(); final MockUserApiRepository userApiRepository = MockUserApiRepository();
final MockPartnerRepository partnerRepository = MockPartnerRepository(); final MockPartnerRepository partnerRepository = MockPartnerRepository();
final MockUserService userService = MockUserService();
final owner = UserDto( final owner = UserDto(
uid: "1", uid: "1",
@ -101,11 +103,12 @@ void main() {
exifInfoRepository, exifInfoRepository,
partnerRepository, partnerRepository,
userRepository, userRepository,
StoreService.I, userService,
eTagRepository, eTagRepository,
partnerApiRepository, partnerApiRepository,
userApiRepository, userApiRepository,
); );
when(() => userService.getMyUser()).thenReturn(owner);
when(() => eTagRepository.get(owner.id)) when(() => eTagRepository.get(owner.id))
.thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now())); .thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now()));
when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});

View File

@ -31,6 +31,8 @@ void main() {
albumMediaRepository = MockAlbumMediaRepository(); albumMediaRepository = MockAlbumMediaRepository();
albumApiRepository = MockAlbumApiRepository(); albumApiRepository = MockAlbumApiRepository();
when(() => userService.getMyUser()).thenReturn(UserStub.user1);
when(() => albumRepository.transaction<void>(any())).thenAnswer( when(() => albumRepository.transaction<void>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(), (call) => (call.positionalArguments.first as Function).call(),
); );
@ -40,6 +42,7 @@ void main() {
sut = AlbumService( sut = AlbumService(
syncService, syncService,
userService,
entityService, entityService,
albumRepository, albumRepository,
assetRepository, assetRepository,