import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/upload.service.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionProvider = NotifierProvider( ActionNotifier.new, dependencies: [ multiSelectProvider, timelineServiceProvider, ], ); class ActionResult { final int count; final bool success; final String? error; const ActionResult({required this.count, required this.success, this.error}); @override String toString() => 'ActionResult(count: $count, success: $success, error: $error)'; } class ActionNotifier extends Notifier { final Logger _logger = Logger('ActionNotifier'); late ActionService _service; late UploadService _uploadService; ActionNotifier() : super(); @override void build() { _uploadService = ref.watch(uploadServiceProvider); _service = ref.watch(actionServiceProvider); } List _getRemoteIdsForSource(ActionSource source) { return _getAssets(source).whereType().toIds().toList(growable: false); } List _getLocalIdsForSource(ActionSource source) { final Set assets = _getAssets(source); final List localIds = []; for (final asset in assets) { if (asset is LocalAsset) { localIds.add(asset.id); } else if (asset is RemoteAsset && asset.localId != null) { localIds.add(asset.localId!); } } return localIds; } List _getOwnedRemoteIdsForSource(ActionSource source) { final ownerId = ref.read(currentUserProvider)?.id; return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); } List _getOwnedRemoteAssetsForSource(ActionSource source) { final ownerId = ref.read(currentUserProvider)?.id; return _getIdsForSource(source).ownedAssets(ownerId).toList(); } Iterable _getIdsForSource(ActionSource source) { final Set assets = _getAssets(source); return switch (T) { const (RemoteAsset) => assets.whereType(), const (LocalAsset) => assets.whereType(), _ => const [], } as Iterable; } Set _getAssets(ActionSource source) { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { BaseAsset asset => {asset}, null => const {}, }, }; } Future shareLink( ActionSource source, BuildContext context, ) async { final ids = _getRemoteIdsForSource(source); try { await _service.shareLink(ids, context); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to create shared link for assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future favorite(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.favorite(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to favorite assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future unFavorite(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.unFavorite(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to unfavorite assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future archive(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.archive(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to archive assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future unArchive(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.unArchive(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to unarchive assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future moveToLockFolder(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); final localIds = _getLocalIdsForSource(source); try { await _service.moveToLockFolder(ids, localIds); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to move assets to lock folder', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future removeFromLockFolder(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.removeFromLockFolder(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to remove assets from lock folder', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future trash(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.trash(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to trash assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future restoreTrash(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.restoreTrash(ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to restore trash assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future trashRemoteAndDeleteLocal(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); final localIds = _getLocalIdsForSource(source); try { await _service.trashRemoteAndDeleteLocal(ids, localIds); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to delete assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future deleteRemoteAndLocal(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); final localIds = _getLocalIdsForSource(source); try { await _service.deleteRemoteAndLocal(ids, localIds); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to delete assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future deleteLocal(ActionSource source) async { final ids = _getLocalIdsForSource(source); try { final deletedCount = await _service.deleteLocal(ids); return ActionResult(count: deletedCount, success: true); } catch (error, stack) { _logger.severe('Failed to delete assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future editLocation( ActionSource source, BuildContext context, ) async { final ids = _getOwnedRemoteIdsForSource(source); try { final isEdited = await _service.editLocation(ids, context); if (!isEdited) { return null; } return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to edit location for assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future removeFromAlbum( ActionSource source, String albumId, ) async { final ids = _getRemoteIdsForSource(source); try { final removedCount = await _service.removeFromAlbum(ids, albumId); return ActionResult(count: removedCount, success: true); } catch (error, stack) { _logger.severe('Failed to remove assets from album', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future stack(String userId, ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { await _service.stack(userId, ids); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to stack assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future unStack(ActionSource source) async { final assets = _getOwnedRemoteAssetsForSource(source); try { await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); return ActionResult(count: assets.length, success: true); } catch (error, stack) { _logger.severe('Failed to unstack assets', error, stack); return ActionResult( count: assets.length, success: false, ); } } Future shareAssets(ActionSource source) async { final ids = _getAssets(source).toList(growable: false); try { final count = await _service.shareAssets(ids); return ActionResult(count: count, success: true); } catch (error, stack) { _logger.severe('Failed to share assets', error, stack); return ActionResult( count: ids.length, success: false, error: error.toString(), ); } } Future downloadAll(ActionSource source) async { final assets = _getAssets(source).whereType().toList(growable: false); try { final didEnqueue = await _service.downloadAll(assets); final enqueueCount = didEnqueue.where((e) => e).length; return ActionResult(count: enqueueCount, success: true); } catch (error, stack) { _logger.severe('Failed to download assets', error, stack); return ActionResult( count: assets.length, success: false, error: error.toString(), ); } } Future upload(ActionSource source) async { final assets = _getAssets(source).whereType().toList(); try { await _uploadService.manualBackup(assets); return ActionResult(count: assets.length, success: true); } catch (error, stack) { _logger.severe('Failed manually upload assets', error, stack); return ActionResult( count: assets.length, success: false, error: error.toString(), ); } } } extension on Iterable { Iterable toIds() => map((e) => e.id); Iterable ownedAssets(String? ownerId) { if (ownerId == null) return const []; return whereType().where((a) => a.ownerId == ownerId); } }