From e0fa3cdbc75817226bebe6eb58dd9a069e112d39 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:24:48 +0200 Subject: [PATCH] refactor(mobile): more repositories (#12879) * ExifInfoRepository * ActivityApiRepository * initial AssetApiRepository --- mobile/analysis_options.yaml | 9 +-- .../interfaces/activity_api.interface.dart | 16 ++++ mobile/lib/interfaces/asset.interface.dart | 12 +++ .../lib/interfaces/asset_api.interface.dart | 16 ++++ .../lib/interfaces/exif_info.interface.dart | 9 +++ .../lib/models/activities/activity.model.dart | 17 ++-- .../providers/activity_service.provider.dart | 4 +- .../activity_statistics.provider.dart | 2 +- .../repositories/activity_api.repository.dart | 67 +++++++++++++++ .../repositories/album_api.repository.dart | 25 +++--- mobile/lib/repositories/asset.repository.dart | 80 ++++++++++++++++++ .../repositories/asset_api.repository.dart | 25 ++++++ .../lib/repositories/base_api.repository.dart | 11 +++ .../repositories/exif_info.repository.dart | 28 +++++++ mobile/lib/services/activity.service.dart | 48 ++++------- mobile/lib/services/asset.service.dart | 52 ++++++++++++ .../services/asset_description.service.dart | 66 --------------- .../services/backup_verification.service.dart | 81 +++++++------------ .../asset_viewer/description_input.dart | 10 ++- .../activity_statistics_provider_test.dart | 7 +- 20 files changed, 392 insertions(+), 193 deletions(-) create mode 100644 mobile/lib/interfaces/activity_api.interface.dart create mode 100644 mobile/lib/interfaces/asset_api.interface.dart create mode 100644 mobile/lib/interfaces/exif_info.interface.dart create mode 100644 mobile/lib/repositories/activity_api.repository.dart create mode 100644 mobile/lib/repositories/asset_api.repository.dart create mode 100644 mobile/lib/repositories/base_api.repository.dart create mode 100644 mobile/lib/repositories/exif_info.repository.dart delete mode 100644 mobile/lib/services/asset_description.service.dart diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 8f9d41d736..e996a54372 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -64,7 +64,7 @@ custom_lint: allowed: # required / wanted - lib/entities/*.entity.dart - - lib/repositories/{album,asset,backup,user}.repository.dart + - lib/repositories/{album,asset,backup,exif_info,user}.repository.dart # acceptable exceptions for the time being - integration_test/test_utils/general_helper.dart - lib/main.dart @@ -75,7 +75,7 @@ custom_lint: - lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart - - lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart + - lib/services/{asset,background,backup,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart - lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart - import_rule_openapi: @@ -83,13 +83,12 @@ custom_lint: restrict: package:openapi allowed: # requried / wanted - - lib/repositories/album_api.repository.dart + - lib/repositories/*_api.repository.dart # acceptable exceptions for the time being - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... # refactor - - lib/models/activities/activity.model.dart - lib/models/map/map_marker.model.dart - lib/models/search/search_filter.model.dart - lib/models/server_info/server_{config,disk_info,features,version}.model.dart @@ -102,7 +101,7 @@ custom_lint: - lib/providers/search/{people,search,search_filter}.provider.dart - lib/providers/websocket.provider.dart - lib/routing/auth_guard.dart - - lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart + - lib/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart - lib/widgets/album/album_thumbnail_listtile.dart - lib/widgets/forms/login/login_form.dart - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart diff --git a/mobile/lib/interfaces/activity_api.interface.dart b/mobile/lib/interfaces/activity_api.interface.dart new file mode 100644 index 0000000000..99aef6f4d4 --- /dev/null +++ b/mobile/lib/interfaces/activity_api.interface.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/models/activities/activity.model.dart'; + +abstract interface class IActivityApiRepository { + Future<List<Activity>> getAll( + String albumId, { + String? assetId, + }); + Future<Activity> create( + String albumId, + ActivityType type, { + String? assetId, + String? comment, + }); + Future<void> delete(String id); + Future<ActivityStats> getStats(String albumId, {String? assetId}); +} diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 2574e52112..98f4c7687c 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -7,4 +7,16 @@ abstract interface class IAssetRepository { Future<List<Asset>> getAllByRemoteId(Iterable<String> ids); Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}); Future<void> deleteById(List<int> ids); + Future<List<Asset>> getAll({ + required int ownerId, + bool? remote, + int limit = 100, + }); + + Future<List<Asset>> getMatches({ + required List<Asset> assets, + required int ownerId, + bool? remote, + int limit = 100, + }); } diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart new file mode 100644 index 0000000000..201c85cea7 --- /dev/null +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/entities/asset.entity.dart'; + +abstract interface class IAssetApiRepository { + // Future<Asset> get(String id); + + // Future<List<Asset>> getAll(); + + // Future<Asset> create(Asset asset); + + Future<Asset> update( + String id, { + String? description, + }); + + // Future<void> delete(String id); +} diff --git a/mobile/lib/interfaces/exif_info.interface.dart b/mobile/lib/interfaces/exif_info.interface.dart new file mode 100644 index 0000000000..fa8ca08f9d --- /dev/null +++ b/mobile/lib/interfaces/exif_info.interface.dart @@ -0,0 +1,9 @@ +import 'package:immich_mobile/entities/exif_info.entity.dart'; + +abstract interface class IExifInfoRepository { + Future<ExifInfo?> get(int id); + + Future<ExifInfo> update(ExifInfo exifInfo); + + Future<void> delete(int id); +} diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 6adb80dca9..4702753f41 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:openapi/api.dart'; enum ActivityType { comment, like } @@ -38,16 +37,6 @@ class Activity { ); } - Activity.fromDto(ActivityResponseDto dto) - : id = dto.id, - assetId = dto.assetId, - comment = dto.comment, - createdAt = dto.createdAt, - type = dto.type == ReactionType.comment - ? ActivityType.comment - : ActivityType.like, - user = User.fromSimpleUserDto(dto.user); - @override String toString() { return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)'; @@ -75,3 +64,9 @@ class Activity { user.hashCode; } } + +class ActivityStats { + final int comments; + + const ActivityStats({required this.comments}); +} diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart index dcfaac883f..6bd139c565 100644 --- a/mobile/lib/providers/activity_service.provider.dart +++ b/mobile/lib/providers/activity_service.provider.dart @@ -1,9 +1,9 @@ +import 'package:immich_mobile/repositories/activity_api.repository.dart'; import 'package:immich_mobile/services/activity.service.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'activity_service.provider.g.dart'; @riverpod ActivityService activityService(ActivityServiceRef ref) => - ActivityService(ref.watch(apiServiceProvider)); + ActivityService(ref.watch(activityApiRepositoryProvider)); diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart index afb43e8cba..b1d2b4b987 100644 --- a/mobile/lib/providers/activity_statistics.provider.dart +++ b/mobile/lib/providers/activity_statistics.provider.dart @@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics { ref .watch(activityServiceProvider) .getStatistics(albumId, assetId: assetId) - .then((comments) => state = comments); + .then((stats) => state = stats.comments); return 0; } diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart new file mode 100644 index 0000000000..0b1b4d99f3 --- /dev/null +++ b/mobile/lib/repositories/activity_api.repository.dart @@ -0,0 +1,67 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/activity_api.interface.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final activityApiRepositoryProvider = Provider( + (ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi), +); + +class ActivityApiRepository extends BaseApiRepository + implements IActivityApiRepository { + final ActivitiesApi _api; + + ActivityApiRepository(this._api); + + @override + Future<List<Activity>> getAll(String albumId, {String? assetId}) async { + final response = + await checkNull(_api.getActivities(albumId, assetId: assetId)); + return response.map(_toActivity).toList(); + } + + @override + Future<Activity> create( + String albumId, + ActivityType type, { + String? assetId, + String? comment, + }) async { + final dto = ActivityCreateDto( + albumId: albumId, + type: type == ActivityType.comment + ? ReactionType.comment + : ReactionType.like, + assetId: assetId, + comment: comment, + ); + final response = await checkNull(_api.createActivity(dto)); + return _toActivity(response); + } + + @override + Future<void> delete(String id) { + return checkNull(_api.deleteActivity(id)); + } + + @override + Future<ActivityStats> getStats(String albumId, {String? assetId}) async { + final response = + await checkNull(_api.getActivityStatistics(albumId, assetId: assetId)); + return ActivityStats(comments: response.comments); + } + + static Activity _toActivity(ActivityResponseDto dto) => Activity( + id: dto.id, + createdAt: dto.createdAt, + type: dto.type == ReactionType.comment + ? ActivityType.comment + : ActivityType.like, + user: User.fromSimpleUserDto(dto.user), + assetId: dto.assetId, + comment: dto.comment, + ); +} diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 6b7865f8e4..0e27e44684 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -1,30 +1,31 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/errors.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:openapi/api.dart'; final albumApiRepositoryProvider = Provider( (ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), ); -class AlbumApiRepository implements IAlbumApiRepository { +class AlbumApiRepository extends BaseApiRepository + implements IAlbumApiRepository { final AlbumsApi _api; AlbumApiRepository(this._api); @override Future<Album> get(String id) async { - final dto = await _checkNull(_api.getAlbumInfo(id)); + final dto = await checkNull(_api.getAlbumInfo(id)); return _toAlbum(dto); } @override Future<List<Album>> getAll({bool? shared}) async { - final dtos = await _checkNull(_api.getAllAlbums(shared: shared)); + final dtos = await checkNull(_api.getAllAlbums(shared: shared)); return dtos.map(_toAlbum).toList().cast(); } @@ -37,7 +38,7 @@ class AlbumApiRepository implements IAlbumApiRepository { final users = sharedUserIds.map( (id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor), ); - final responseDto = await _checkNull( + final responseDto = await checkNull( _api.createAlbum( CreateAlbumDto( albumName: name, @@ -57,7 +58,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String? description, bool? activityEnabled, }) async { - final response = await _checkNull( + final response = await checkNull( _api.updateAlbumInfo( albumId, UpdateAlbumDto( @@ -81,7 +82,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String albumId, Iterable<String> assetIds, ) async { - final response = await _checkNull( + final response = await checkNull( _api.addAssetsToAlbum( albumId, BulkIdsDto(ids: assetIds.toList()), @@ -106,7 +107,7 @@ class AlbumApiRepository implements IAlbumApiRepository { String albumId, Iterable<String> assetIds, ) async { - final response = await _checkNull( + final response = await checkNull( _api.removeAssetFromAlbum( albumId, BulkIdsDto(ids: assetIds.toList()), @@ -127,7 +128,7 @@ class AlbumApiRepository implements IAlbumApiRepository { Future<Album> addUsers(String albumId, Iterable<String> userIds) async { final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList(); - final response = await _checkNull( + final response = await checkNull( _api.addUsersToAlbum( albumId, AddUsersDto(albumUsers: albumUsers), @@ -141,12 +142,6 @@ class AlbumApiRepository implements IAlbumApiRepository { return _api.removeUserFromAlbum(albumId, userId); } - static Future<T> _checkNull<T>(Future<T?> future) async { - final response = await future; - if (response == null) throw NoResponseDtoError(); - return response; - } - static Album _toAlbum(AlbumResponseDto dto) { final Album album = Album( remoteId: dto.id, diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index 8ec028f728..c6012af371 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -35,4 +35,84 @@ class AssetRepository implements IAssetRepository { @override Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) => _db.assets.getAllByRemoteId(ids); + + @override + Future<List<Asset>> getAll({ + required int ownerId, + bool? remote, + int limit = 100, + }) { + if (remote == null) { + return _db.assets + .where() + .ownerIdEqualToAnyChecksum(ownerId) + .limit(limit) + .findAll(); + } + final QueryBuilder<Asset, Asset, QAfterFilterCondition> query; + if (remote) { + query = _db.assets + .where() + .localIdIsNull() + .filter() + .remoteIdIsNotNull() + .ownerIdEqualTo(ownerId); + } else { + query = _db.assets + .where() + .remoteIdIsNull() + .filter() + .localIdIsNotNull() + .ownerIdEqualTo(ownerId); + } + + return query.limit(limit).findAll(); + } + + @override + Future<List<Asset>> getMatches({ + required List<Asset> assets, + required int ownerId, + bool? remote, + int limit = 100, + }) { + final QueryBuilder<Asset, Asset, QAfterFilterCondition> query; + if (remote == null) { + query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull(); + } else if (remote) { + query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(); + } else { + query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(); + } + return _getMatchesImpl(query, ownerId, assets, limit); + } } + +Future<List<Asset>> _getMatchesImpl( + QueryBuilder<Asset, Asset, QAfterFilterCondition> query, + int ownerId, + List<Asset> assets, + int limit, +) => + query + .ownerIdEqualTo(ownerId) + .anyOf( + assets, + (q, Asset a) => q + .fileNameEqualTo(a.fileName) + .and() + .durationInSecondsEqualTo(a.durationInSeconds) + .and() + .fileCreatedAtBetween( + a.fileCreatedAt.subtract(const Duration(hours: 12)), + a.fileCreatedAt.add(const Duration(hours: 12)), + ) + .and() + .not() + .checksumEqualTo(a.checksum), + ) + .sortByFileName() + .thenByFileCreatedAt() + .thenByFileModifiedAt() + .limit(limit) + .findAll(); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart new file mode 100644 index 0000000000..3ad0e1cba0 --- /dev/null +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -0,0 +1,25 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final assetApiRepositoryProvider = Provider( + (ref) => AssetApiRepository(ref.watch(apiServiceProvider).assetsApi), +); + +class AssetApiRepository extends BaseApiRepository + implements IAssetApiRepository { + final AssetsApi _api; + + AssetApiRepository(this._api); + + @override + Future<Asset> update(String id, {String? description}) async { + final response = await checkNull( + _api.updateAsset(id, UpdateAssetDto(description: description)), + ); + return Asset.remote(response); + } +} diff --git a/mobile/lib/repositories/base_api.repository.dart b/mobile/lib/repositories/base_api.repository.dart new file mode 100644 index 0000000000..418cba84f8 --- /dev/null +++ b/mobile/lib/repositories/base_api.repository.dart @@ -0,0 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/constants/errors.dart'; + +abstract class BaseApiRepository { + @protected + Future<T> checkNull<T>(Future<T?> future) async { + final response = await future; + if (response == null) throw NoResponseDtoError(); + return response; + } +} diff --git a/mobile/lib/repositories/exif_info.repository.dart b/mobile/lib/repositories/exif_info.repository.dart new file mode 100644 index 0000000000..a165e98bdb --- /dev/null +++ b/mobile/lib/repositories/exif_info.repository.dart @@ -0,0 +1,28 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final exifInfoRepositoryProvider = + Provider((ref) => ExifInfoRepository(ref.watch(dbProvider))); + +class ExifInfoRepository implements IExifInfoRepository { + final Isar _db; + + ExifInfoRepository( + this._db, + ); + + @override + Future<void> delete(int id) => _db.exifInfos.delete(id); + + @override + Future<ExifInfo?> get(int id) => _db.exifInfos.get(id); + + @override + Future<ExifInfo> update(ExifInfo exifInfo) async { + await _db.writeTxn(() => _db.exifInfos.put(exifInfo)); + return exifInfo; + } +} diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 58af26e204..5496041416 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -1,41 +1,31 @@ -import 'package:immich_mobile/constants/errors.dart'; +import 'package:immich_mobile/interfaces/activity_api.interface.dart'; import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; class ActivityService with ErrorLoggerMixin { - final ApiService _apiService; + final IActivityApiRepository _activityApiRepository; @override final Logger logger = Logger("ActivityService"); - ActivityService(this._apiService); + ActivityService(this._activityApiRepository); Future<List<Activity>> getAllActivities( String albumId, { String? assetId, }) async { return logError( - () async { - final list = await _apiService.activitiesApi - .getActivities(albumId, assetId: assetId); - return list != null ? list.map(Activity.fromDto).toList() : []; - }, + () => _activityApiRepository.getAll(albumId, assetId: assetId), defaultValue: [], errorMessage: "Failed to get all activities for album $albumId", ); } - Future<int> getStatistics(String albumId, {String? assetId}) async { + Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async { return logError( - () async { - final dto = await _apiService.activitiesApi - .getActivityStatistics(albumId, assetId: assetId); - return dto?.comments ?? 0; - }, - defaultValue: 0, + () => _activityApiRepository.getStats(albumId, assetId: assetId), + defaultValue: const ActivityStats(comments: 0), errorMessage: "Failed to statistics for album $albumId", ); } @@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin { Future<bool> removeActivity(String id) async { return logError( () async { - await _apiService.activitiesApi.deleteActivity(id); + await _activityApiRepository.delete(id); return true; }, defaultValue: false, @@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin { String? comment, }) async { return guardError( - () async { - final dto = await _apiService.activitiesApi.createActivity( - ActivityCreateDto( - albumId: albumId, - type: type == ActivityType.comment - ? ReactionType.comment - : ReactionType.like, - assetId: assetId, - comment: comment, - ), - ); - if (dto != null) { - return Activity.fromDto(dto); - } - throw NoResponseDtoError(); - }, + () => _activityApiRepository.create( + albumId, + type, + assetId: assetId, + comment: comment, + ), errorMessage: "Failed to create $type for album $albumId", ); } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 90c46ae90a..262040026e 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -9,9 +9,13 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -24,6 +28,8 @@ import 'package:openapi/api.dart'; final assetServiceProvider = Provider( (ref) => AssetService( + ref.watch(assetApiRepositoryProvider), + ref.watch(exifInfoRepositoryProvider), ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), @@ -34,6 +40,8 @@ final assetServiceProvider = Provider( ); class AssetService { + final IAssetApiRepository _assetApiRepository; + final IExifInfoRepository _exifInfoRepository; final ApiService _apiService; final SyncService _syncService; final UserService _userService; @@ -43,6 +51,8 @@ class AssetService { final Isar _db; AssetService( + this._assetApiRepository, + this._exifInfoRepository, this._apiService, this._syncService, this._userService, @@ -342,4 +352,46 @@ class AssetService { log.severe("Error while syncing uploaded asset to albums", error, stack); } } + + Future<void> setDescription( + Asset asset, + String newDescription, + ) async { + final remoteAssetId = asset.remoteId; + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (remoteAssetId == null || localExifId == null) { + return; + } + + final result = await _assetApiRepository.update( + remoteAssetId, + description: newDescription, + ); + + final description = result.exifInfo?.description; + + if (description != null) { + var exifInfo = await _exifInfoRepository.get(localExifId); + + if (exifInfo != null) { + exifInfo.description = description; + await _exifInfoRepository.update(exifInfo); + } + } + } + + Future<String> getDescription(Asset asset) async { + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (localExifId == null) { + return ""; + } + + final exifInfo = await _exifInfoRepository.get(localExifId); + + return exifInfo?.description ?? ""; + } } diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart deleted file mode 100644 index 196e29dc6a..0000000000 --- a/mobile/lib/services/asset_description.service.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; -import 'package:openapi/api.dart'; - -class AssetDescriptionService { - AssetDescriptionService(this._db, this._api); - - final Isar _db; - final ApiService _api; - - Future<void> setDescription( - Asset asset, - String newDescription, - ) async { - final remoteAssetId = asset.remoteId; - final localExifId = asset.exifInfo?.id; - - // Guard [remoteAssetId] and [localExifId] null - if (remoteAssetId == null || localExifId == null) { - return; - } - - final result = await _api.assetsApi.updateAsset( - remoteAssetId, - UpdateAssetDto(description: newDescription), - ); - - final description = result?.exifInfo?.description; - - if (description != null) { - var exifInfo = await _db.exifInfos.get(localExifId); - - if (exifInfo != null) { - exifInfo.description = description; - await _db.writeTxn( - () => _db.exifInfos.put(exifInfo), - ); - } - } - } - - String getAssetDescription(Asset asset) { - final localExifId = asset.exifInfo?.id; - - // Guard [remoteAssetId] and [localExifId] null - if (localExifId == null) { - return ""; - } - - final exifInfo = _db.exifInfos.getSync(localExifId); - - return exifInfo?.description ?? ""; - } -} - -final assetDescriptionServiceProvider = Provider( - (ref) => AssetDescriptionService( - ref.watch(dbProvider), - ref.watch(apiServiceProvider), - ), -); diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 66a61d2914..da9d8da164 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -8,41 +8,46 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; /// Finds duplicates originating from missing EXIF information class BackupVerificationService { - final Isar _db; final IFileMediaRepository _fileMediaRepository; + final IAssetRepository _assetRepository; + final IExifInfoRepository _exifInfoRepository; - BackupVerificationService(this._db, this._fileMediaRepository); + BackupVerificationService( + this._fileMediaRepository, + this._assetRepository, + this._exifInfoRepository, + ); /// Returns at most [limit] assets that were backed up without exif Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async { final owner = Store.get(StoreKey.currentUser).isarId; - final List<Asset> onlyLocal = await _db.assets - .where() - .remoteIdIsNull() - .filter() - .ownerIdEqualTo(owner) - .localIdIsNotNull() - .findAll(); - final List<Asset> remoteMatches = await _getMatches( - _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(), - owner, - onlyLocal, - limit, + final List<Asset> onlyLocal = await _assetRepository.getAll( + ownerId: owner, + remote: false, + limit: limit, ); - final List<Asset> localMatches = await _getMatches( - _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(), - owner, - remoteMatches, - limit, + final List<Asset> remoteMatches = await _assetRepository.getMatches( + assets: onlyLocal, + ownerId: owner, + remote: true, + limit: limit, + ); + final List<Asset> localMatches = await _assetRepository.getMatches( + assets: remoteMatches, + ownerId: owner, + remote: false, + limit: limit, ); final List<Asset> deleteCandidates = [], originals = []; @@ -52,7 +57,7 @@ class BackupVerificationService { localMatches, compare: (a, b) => a.fileName.compareTo(b.fileName), both: (a, b) async { - a.exifInfo = await _db.exifInfos.get(a.id); + a.exifInfo = await _exifInfoRepository.get(a.id); deleteCandidates.add(a); originals.add(b); return false; @@ -192,35 +197,6 @@ class BackupVerificationService { return bytes.buffer.asUint64List(start); } - static Future<List<Asset>> _getMatches( - QueryBuilder<Asset, Asset, QAfterFilterCondition> query, - int ownerId, - List<Asset> assets, - int limit, - ) => - query - .ownerIdEqualTo(ownerId) - .anyOf( - assets, - (q, Asset a) => q - .fileNameEqualTo(a.fileName) - .and() - .durationInSecondsEqualTo(a.durationInSeconds) - .and() - .fileCreatedAtBetween( - a.fileCreatedAt.subtract(const Duration(hours: 12)), - a.fileCreatedAt.add(const Duration(hours: 12)), - ) - .and() - .not() - .checksumEqualTo(a.checksum), - ) - .sortByFileName() - .thenByFileCreatedAt() - .thenByFileModifiedAt() - .limit(limit) - .findAll(); - static bool _sameExceptTimeZone(DateTime a, DateTime b) { final ms = a.isAfter(b) ? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch @@ -233,7 +209,8 @@ class BackupVerificationService { final backupVerificationServiceProvider = Provider( (ref) => BackupVerificationService( - ref.watch(dbProvider), ref.watch(fileMediaRepositoryProvider), + ref.watch(assetRepositoryProvider), + ref.watch(exifInfoRepositoryProvider), ), ); diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 18ef394e2d..3fdd40130a 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset_description.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; @@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget { final focusNode = useFocusNode(); final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); - final descriptionProvider = ref.watch(assetDescriptionServiceProvider); + final assetService = ref.watch(assetServiceProvider); final owner = ref.watch(currentUserProvider); final hasError = useState(false); final assetWithExif = ref.watch(assetDetailProvider(asset)); useEffect( () { - controller.text = descriptionProvider.getAssetDescription(asset); + assetService + .getDescription(asset) + .then((value) => controller.text = value); return null; }, [assetWithExif.value], @@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget { submitDescription(String description) async { hasError.value = false; try { - await descriptionProvider.setDescription( + await assetService.setDescription( asset, description, ); diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart index 9edabcc0d0..0216528ddd 100644 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:mocktail/mocktail.dart'; @@ -25,7 +26,7 @@ void main() { test('Returns the proper count family', () async { when( () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 5); + ).thenAnswer((_) async => const ActivityStats(comments: 5)); // Read here to make the getStatistics call container.read(activityStatisticsProvider('test-album', 'test-asset')); @@ -50,7 +51,7 @@ void main() { test('Adds activity', () async { when( () => activityMock.getStatistics('test-album'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('test-album'); container.listen( @@ -71,7 +72,7 @@ void main() { test('Removes activity', () async { when( () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('new-album', 'test-asset'); container.listen(