mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: manually upload local assets to album
This commit is contained in:
@@ -9,12 +9,47 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Categorizes a heterogeneous asset selection into the candidates that can
|
||||
/// be added to an album immediately (already on the server) and the local-only
|
||||
/// candidates that must be uploaded first.
|
||||
class AlbumAssetCandidates {
|
||||
final List<String> remoteAssetIds;
|
||||
final List<LocalAsset> localAssetsToUpload;
|
||||
|
||||
const AlbumAssetCandidates({required this.remoteAssetIds, required this.localAssetsToUpload});
|
||||
}
|
||||
|
||||
class RemoteAlbumService {
|
||||
static final _logger = Logger('RemoteAlbumService');
|
||||
|
||||
final DriftRemoteAlbumRepository _repository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final ForegroundUploadService _uploadService;
|
||||
|
||||
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||
const RemoteAlbumService(this._repository, this._albumApiRepository, this._uploadService);
|
||||
|
||||
/// Categorizes a heterogeneous asset selection into already-on-server IDs
|
||||
/// and local assets that still need to be uploaded.
|
||||
static AlbumAssetCandidates categorizeCandidates(Iterable<BaseAsset> assets) {
|
||||
final remoteIds = <String>[];
|
||||
final localToUpload = <LocalAsset>[];
|
||||
for (final asset in assets) {
|
||||
if (asset is RemoteAsset) {
|
||||
remoteIds.add(asset.id);
|
||||
} else if (asset is LocalAsset) {
|
||||
final remoteId = asset.remoteId;
|
||||
if (remoteId != null) {
|
||||
remoteIds.add(remoteId);
|
||||
} else {
|
||||
localToUpload.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
return AlbumAssetCandidates(remoteAssetIds: remoteIds, localAssetsToUpload: localToUpload);
|
||||
}
|
||||
|
||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||
return _repository.watchAlbum(albumId);
|
||||
@@ -148,6 +183,99 @@ class RemoteAlbumService {
|
||||
return album.added.length;
|
||||
}
|
||||
|
||||
/// Adds an asset selection to an album, uploading any local-only assets
|
||||
/// first and linking each one as soon as its upload completes.
|
||||
Future<int> addAssetsToAlbum({
|
||||
required String albumId,
|
||||
required UserDto uploader,
|
||||
required AlbumAssetCandidates candidates,
|
||||
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||
}) async {
|
||||
int addedCount = 0;
|
||||
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
||||
}
|
||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||
addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks);
|
||||
}
|
||||
return addedCount;
|
||||
}
|
||||
|
||||
/// Creates an album, seeding it with already-remote asset IDs, then uploads
|
||||
/// local-only assets and links each one as it finishes.
|
||||
Future<RemoteAlbum> createAlbumWithAssets({
|
||||
required String title,
|
||||
required UserDto owner,
|
||||
String? description,
|
||||
AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []),
|
||||
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
|
||||
}) async {
|
||||
final album = await createAlbum(
|
||||
title: title,
|
||||
owner: owner,
|
||||
description: description,
|
||||
assetIds: candidates.remoteAssetIds,
|
||||
);
|
||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||
await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks);
|
||||
}
|
||||
return album;
|
||||
}
|
||||
|
||||
Future<int> _uploadAndAddLocals(
|
||||
String albumId,
|
||||
UserDto uploader,
|
||||
List<LocalAsset> localAssets,
|
||||
UploadCallbacks userCallbacks,
|
||||
) async {
|
||||
int addedCount = 0;
|
||||
final pendingAdds = <Future<void>>[];
|
||||
final localById = {for (final a in localAssets) a.id: a};
|
||||
|
||||
final wrappedCallbacks = UploadCallbacks(
|
||||
onProgress: userCallbacks.onProgress,
|
||||
onICloudProgress: userCallbacks.onICloudProgress,
|
||||
onError: userCallbacks.onError,
|
||||
onSuccess: (localId, remoteId) {
|
||||
userCallbacks.onSuccess?.call(localId, remoteId);
|
||||
final source = localById[localId];
|
||||
if (source == null) {
|
||||
_logger.warning('Upload success for $localId but source LocalAsset missing; skipping album link');
|
||||
return;
|
||||
}
|
||||
pendingAdds.add(
|
||||
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||
.then<void>((added) {
|
||||
addedCount += added;
|
||||
})
|
||||
.catchError((Object error, StackTrace stack) {
|
||||
_logger.warning('Failed to add uploaded asset $remoteId to album $albumId', error, stack);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks);
|
||||
await Future.wait(pendingAdds);
|
||||
return addedCount;
|
||||
}
|
||||
|
||||
/// Links a freshly-uploaded asset to an album, ensuring the local DB
|
||||
/// reflects the change without waiting for the next sync. We call the API
|
||||
/// (server is the source of truth), then upsert a placeholder
|
||||
/// `remote_asset_entity` row from the local source so the FK-protected
|
||||
/// junction insert succeeds. Sync overwrites the placeholder later with
|
||||
/// the authoritative server data.
|
||||
Future<int> _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
|
||||
final result = await _albumApiRepository.addAssets(albumId, [remoteId]);
|
||||
if (result.added.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
await _repository.upsertRemoteAssetStub(remoteId: remoteId, ownerId: uploader.id, source: source);
|
||||
await _repository.addAssets(albumId, result.added);
|
||||
return result.added.length;
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) async {
|
||||
await _albumApiRepository.deleteAlbum(albumId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user