import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:openapi/api.dart' as api show AssetVisibility; import 'package:openapi/api.dart' hide AssetVisibility; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( ref.watch(apiServiceProvider).assetsApi, ref.watch(apiServiceProvider).searchApi, ref.watch(apiServiceProvider).stacksApi, ref.watch(apiServiceProvider).trashApi, ), ); class AssetApiRepository extends ApiRepository { final AssetsApi _api; final SearchApi _searchApi; final StacksApi _stacksApi; final TrashApi _trashApi; AssetApiRepository(this._api, this._searchApi, this._stacksApi, this._trashApi); Future update(String id, {String? description}) async { final response = await checkNull(_api.updateAsset(id, UpdateAssetDto(description: description))); return Asset.remote(response); } Future> search({List personIds = const []}) async { // TODO this always fetches all assets, change API and usage to actually do pagination final List result = []; bool hasNext = true; int currentPage = 1; while (hasNext) { final response = await checkNull( _searchApi.searchAssets(MetadataSearchDto(personIds: personIds, page: currentPage, size: 1000)), ); result.addAll(response.assets.items.map(Asset.remote)); hasNext = response.assets.nextPage != null; currentPage++; } return result; } Future delete(List ids, bool force) async { return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force)); } Future restoreTrash(List ids) async { await _trashApi.restoreAssets(BulkIdsDto(ids: ids)); } Future updateVisibility(List ids, AssetVisibilityEnum visibility) async { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility))); } Future updateFavorite(List ids, bool isFavorite) async { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite)); } Future updateLocation(List ids, LatLng location) async { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude)); } Future updateDateTime(List ids, DateTime dateTime) async { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: dateTime.toIso8601String())); } Future stack(List ids) async { final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids))); return responseDto.toStack(); } Future unStack(List ids) async { return _stacksApi.deleteStacks(BulkIdsDto(ids: ids)); } Future downloadAsset(String id) { return _api.downloadAssetWithHttpInfo(id); } _mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) { AssetVisibilityEnum.timeline => AssetVisibility.timeline, AssetVisibilityEnum.hidden => AssetVisibility.hidden, AssetVisibilityEnum.locked => AssetVisibility.locked, AssetVisibilityEnum.archive => AssetVisibility.archive, }; Future getAssetMIMEType(String assetId) async { final response = await checkNull(_api.getAssetInfo(assetId)); // we need to get the MIME of the thumbnail once that gets added to the API return response.originalMimeType; } Future updateDescription(String assetId, String description) { return _api.updateAsset(assetId, UpdateAssetDto(description: description)); } Future uploadAsset(XFile file) async { final lastModified = await file.lastModified(); final deviceAssetId = "MOBILE-${file.name}-${lastModified.millisecondsSinceEpoch}"; final multipart = MultipartFile.fromBytes( 'assetData', // field should be 'assetData' to match the backend API await file.readAsBytes(), filename: file.name, ); final asset = await _api.uploadAsset(multipart, deviceAssetId, "MOBILE", lastModified, lastModified); return asset?.id; } } extension on StackResponseDto { StackResponse toStack() { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } extension RemoteAssetDtoExt on AssetResponseDto { RemoteAsset toDto() { return RemoteAsset( id: id, name: originalFileName, checksum: checksum, createdAt: fileCreatedAt, updatedAt: updatedAt, ownerId: ownerId, visibility: switch (visibility) { api.AssetVisibility.timeline => AssetVisibility.timeline, api.AssetVisibility.hidden => AssetVisibility.hidden, api.AssetVisibility.archive => AssetVisibility.archive, api.AssetVisibility.locked => AssetVisibility.locked, _ => AssetVisibility.timeline, }, durationInSeconds: duration.toDuration()?.inSeconds ?? 0, height: exifInfo?.exifImageHeight?.toInt(), width: exifInfo?.exifImageWidth?.toInt(), isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, thumbHash: thumbhash, localId: null, type: type.toType(), ); } } extension on AssetTypeEnum { AssetType toType() => switch (this) { AssetTypeEnum.IMAGE => AssetType.image, AssetTypeEnum.VIDEO => AssetType.video, AssetTypeEnum.AUDIO => AssetType.audio, AssetTypeEnum.OTHER => AssetType.other, _ => throw Exception('Unknown AssetType value: $this'), }; }