diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 35a8f899a8..ca6124e390 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -183,6 +183,12 @@ class RemoteAlbumService { return album.added.length; } + Future removeAssets({required String albumId, required List assetIds}) async { + final result = await _albumApiRepository.removeAssets(albumId, assetIds); + await _repository.removeAssets(albumId, result.removed); + return result.removed.length; + } + /// !TODO The name here is not clear as we have addAssets method above, /// which is only add remote assets to album, for the next PR, we will allow /// adding local assets from album from the timeline as well with this flow. diff --git a/mobile/lib/presentation/actions/remove_from_album.action.dart b/mobile/lib/presentation/actions/remove_from_album.action.dart new file mode 100644 index 0000000000..812321b1ac --- /dev/null +++ b/mobile/lib/presentation/actions/remove_from_album.action.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/asset_filter.dart'; + +class RemoveFromAlbumAction extends BaseAction { + final String albumId; + final List assetIds; + + RemoveFromAlbumAction._({required super.scope, required this.albumId, required this.assetIds, super.isVisible}) + : super(icon: Icons.remove_circle_outline, label: scope.context.t.remove_from_album); + + factory RemoveFromAlbumAction({ + required Iterable assets, + required String albumId, + required ActionScope scope, + }) { + final assetIds = AssetFilter(assets).remote().map((asset) => asset.id).toList(growable: false); + + return RemoveFromAlbumAction._(scope: scope, albumId: albumId, assetIds: assetIds, isVisible: assetIds.isNotEmpty); + } + + @override + Future onAction() async { + final ActionScope(:ref, :context) = scope; + + final count = await ref.read(remoteAlbumServiceProvider).removeAssets(albumId: albumId, assetIds: assetIds); + ref.read(toastRepositoryProvider).success(context.t.remove_from_album_action_prompt(count: count)); + } +} diff --git a/mobile/lib/presentation/actions/set_album_cover.action.dart b/mobile/lib/presentation/actions/set_album_cover.action.dart new file mode 100644 index 0000000000..c6d1300396 --- /dev/null +++ b/mobile/lib/presentation/actions/set_album_cover.action.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/asset_filter.dart'; + +class SetAlbumCoverAction extends BaseAction { + final String albumId; + final List assetIds; + + SetAlbumCoverAction._({required super.scope, required this.albumId, required this.assetIds, super.isVisible}) + : super(icon: Icons.image_outlined, label: scope.context.t.set_as_album_cover); + + factory SetAlbumCoverAction({ + required Iterable assets, + required String albumId, + required ActionScope scope, + }) { + final assetIds = AssetFilter(assets).remote().map((asset) => asset.id).toList(growable: false); + + return SetAlbumCoverAction._(scope: scope, albumId: albumId, assetIds: assetIds, isVisible: assetIds.length == 1); + } + + @override + Future onAction() async { + final ActionScope(:ref, :context) = scope; + + await ref.read(remoteAlbumServiceProvider).updateAlbum(albumId, thumbnailAssetId: assetIds.first); + ref.read(toastRepositoryProvider).success(context.t.album_cover_updated); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart deleted file mode 100644 index 97a36a56dc..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RemoveFromAlbumActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RemoveFromAlbumActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'remove_from_album_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.remove_circle_outline, - label: "remove_from_album".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart deleted file mode 100644 index 1d704aafe8..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class SetAlbumCoverActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const SetAlbumCoverActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - - final successMessage = 'album_cover_updated'.t(context: context); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.image_outlined, - label: 'set_as_album_cover'.t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 69a54bbe32..d4c6c58827 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -6,14 +6,14 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_actions.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; import 'package:immich_mobile/presentation/actions/timeline.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; @@ -83,7 +83,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState } final scope = ActionScope.from(context, ref); - final actions = AssetActions.from(scope, multiselect.selectedAssets.toList(growable: false)); + final assets = multiselect.selectedAssets.toList(growable: false); + final actions = AssetActions.from(scope, assets); return BaseBottomSheet( controller: sheetController, @@ -114,9 +115,18 @@ class _RemoteAlbumBottomSheetState extends ConsumerState ], ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), - if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), - if (ownsAlbum && multiselect.selectedAssets.length == 1) - SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), + if (ownsAlbum) + ActionColumnButtonWidget( + action: TimelineAction( + action: RemoveFromAlbumAction(assets: assets, albumId: widget.album.id, scope: scope), + ), + ), + if (ownsAlbum) + ActionColumnButtonWidget( + action: TimelineAction( + action: SetAlbumCoverAction(assets: assets, albumId: widget.album.id, scope: scope), + ), + ), ], slivers: ownsAlbum ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)] diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d340f8b64d..cd565da4ed 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -318,33 +318,6 @@ class ActionNotifier extends Notifier { ); } - 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 setAlbumCover(ActionSource source, String albumId) async { - final assets = _getAssets(source); - final asset = assets.first; - if (asset is! RemoteAsset) { - return const ActionResult(count: 1, success: false, error: 'Asset must be remote'); - } - - try { - await _service.setAlbumCover(albumId, asset.id); - return const ActionResult(count: 1, success: true); - } catch (error, stack) { - _logger.severe('Failed to set album cover', error, stack); - return ActionResult(count: 1, success: false, error: error.toString()); - } - } - Future updateDescription(ActionSource source, String description) async { final ids = _getRemoteIdsForSource(source); if (ids.length != 1) { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 91e2af8901..9ca699afec 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -199,14 +199,6 @@ class ActionService { ); } - Future removeFromAlbum(List remoteIds, String albumId) async { - final result = await _albumApiRepository.removeAssets(albumId, remoteIds); - if (result.removed.isNotEmpty) { - await _remoteAlbumRepository.removeAssets(albumId, result.removed); - } - return result.removed.length; - } - Future updateDescription(String assetId, String description) async { // update remote first, then local to ensure consistency await _assetApiRepository.updateDescription(assetId, description); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index d476a3de50..46ee405765 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -16,7 +16,9 @@ import 'package:immich_mobile/presentation/actions/cast.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; @@ -26,8 +28,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_a import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; @@ -231,17 +231,11 @@ enum ActionButtonType { menuItem: menuItem, ), ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.removeFromAlbum => ActionMenuItemWidget( + action: RemoveFromAlbumAction(assets: [context.asset], albumId: context.currentAlbum!.id, scope: scope), ), - ActionButtonType.setAlbumCover => SetAlbumCoverActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.setAlbumCover => ActionMenuItemWidget( + action: SetAlbumCoverAction(assets: [context.asset], albumId: context.currentAlbum!.id, scope: scope), ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => ActionMenuItemWidget( diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 8d67ad4ffb..09288ec882 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,5 +1,6 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; @@ -22,4 +23,6 @@ class MockAssetService extends Mock implements AssetService {} class MockUserService extends Mock implements UserService {} +class MockRemoteAlbumService extends Mock implements RemoteAlbumService {} + class MockGCastService extends Mock implements GCastService {} diff --git a/mobile/test/unit/factories/remote_album_factory.dart b/mobile/test/unit/factories/remote_album_factory.dart new file mode 100644 index 0000000000..bffe62b651 --- /dev/null +++ b/mobile/test/unit/factories/remote_album_factory.dart @@ -0,0 +1,38 @@ +import 'package:immich_mobile/domain/models/album/album.model.dart'; + +import '../../utils.dart'; + +class RemoteAlbumFactory { + const RemoteAlbumFactory(); + + static RemoteAlbum create({ + String? id, + String? name, + String? ownerId, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? thumbnailAssetId, + bool isActivityEnabled = false, + AlbumAssetOrder order = AlbumAssetOrder.desc, + int assetCount = 0, + String? ownerName, + bool isShared = false, + }) { + id = TestUtils.uuid(id); + return RemoteAlbum( + id: id, + name: name ?? 'remote_album_$id', + ownerId: TestUtils.uuid(ownerId), + description: description ?? '', + createdAt: TestUtils.date(createdAt), + updatedAt: TestUtils.date(updatedAt), + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + assetCount: assetCount, + ownerName: ownerName ?? 'owner_$id', + isShared: isShared, + ); + } +} diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index f637324701..c537f8ac28 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -13,6 +14,7 @@ import '../domain/service.mock.dart'; import '../infrastructure/repository.mock.dart'; import 'factories/local_album_factory.dart'; import 'factories/local_asset_factory.dart'; +import 'factories/remote_album_factory.dart'; import 'factories/user_factory.dart'; class RepositoryMocks { @@ -58,6 +60,7 @@ class ServiceMocks { final partner = PartnerServiceStub(MockPartnerService()); final user = UserServiceStub(MockUserService()); final asset = AssetServiceStub(MockAssetService()); + final album = RemoteAlbumServiceStub(MockRemoteAlbumService()); ServiceMocks() { resetAll(); @@ -68,9 +71,11 @@ class ServiceMocks { partner.reset(); user.reset(); asset.reset(); + album.reset(); _stubUserService(); _stubPartnerService(); _stubAssetService(); + _stubRemoteAlbumService(); } void _stubUserService() { @@ -96,6 +101,11 @@ class ServiceMocks { when(asset.unstack).thenAnswer((_) async {}); when(asset.restoreTrash).thenAnswer((_) async {}); } + + void _stubRemoteAlbumService() { + when(album.removeAssets).thenAnswer((_) async => 0); + when(album.updateAlbum).thenAnswer((_) async => RemoteAlbumFactory.create()); + } } void _registerFallbacks() { @@ -191,6 +201,17 @@ extension type const AssetServiceStub(MockAssetService service) implements Stub< () => service.restoreTrash(any()); } +extension type const RemoteAlbumServiceStub(MockRemoteAlbumService service) implements Stub { + Future Function() get removeAssets => + () => service.removeAssets( + albumId: any(named: 'albumId'), + assetIds: any(named: 'assetIds'), + ); + + Future Function() get updateAlbum => + () => service.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId')); +} + extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub { Future> Function() get hashAssets => () => api.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess')); diff --git a/mobile/test/unit/presentation/actions/remove_from_album_action_test.dart b/mobile/test/unit/presentation/actions/remove_from_album_action_test.dart new file mode 100644 index 0000000000..54575bb0a4 --- /dev/null +++ b/mobile/test/unit/presentation/actions/remove_from_album_action_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../domain/service.mock.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockRemoteAlbumService albumService; + + setUp(() async { + context = await PresentationContext.create(); + albumService = context.service.album.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset remote() => RemoteAssetFactory.create(ownerId: context.currentUser.id); + + group('RemoveFromAlbumAction', () { + testWidgets('removes the selected remote assets from the album', (tester) async { + final first = remote(); + final second = remote(); + final albumId = 'album'; + final action = await tester.pumpTestAction( + context, + (scope) => RemoveFromAlbumAction(assets: [first, second], albumId: albumId, scope: scope), + ); + + expect(action.icon, Icons.remove_circle_outline); + expect(action.label, StaticTranslations.instance.remove_from_album); + verify(() => albumService.removeAssets(albumId: albumId, assetIds: [first.id, second.id])).called(1); + }); + + testWidgets('reports the removed count through the toast repository', (tester) async { + final toast = context.repository.toast; + final albumId = 'album'; + when(context.service.album.removeAssets).thenAnswer((_) async => 2); + + await tester.pumpTestAction( + context, + (scope) => RemoveFromAlbumAction(assets: [remote(), remote()], albumId: albumId, scope: scope), + ); + + final message = verify(() => toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.remove_from_album_action_prompt(count: 2)); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/set_album_cover_action_test.dart b/mobile/test/unit/presentation/actions/set_album_cover_action_test.dart new file mode 100644 index 0000000000..bc26b4ab31 --- /dev/null +++ b/mobile/test/unit/presentation/actions/set_album_cover_action_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../domain/service.mock.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockRemoteAlbumService albumService; + + setUp(() async { + context = await PresentationContext.create(); + albumService = context.service.album.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset remote() => RemoteAssetFactory.create(ownerId: context.currentUser.id); + + group('SetAlbumCoverAction', () { + testWidgets('sets the selected asset as the album cover', (tester) async { + final asset = remote(); + const albumId = 'album'; + final action = await tester.pumpTestAction( + context, + (scope) => SetAlbumCoverAction(assets: [asset], albumId: albumId, scope: scope), + ); + + expect(action.icon, Icons.image_outlined); + expect(action.label, StaticTranslations.instance.set_as_album_cover); + verify(() => albumService.updateAlbum(albumId, thumbnailAssetId: asset.id)).called(1); + }); + + testWidgets('is hidden unless exactly one remote asset is selected', (tester) async { + const albumId = 'album'; + final action = await tester.pumpActionButton( + context, + (scope) => SetAlbumCoverAction(assets: [remote(), remote()], albumId: albumId, scope: scope), + ); + + expect(action.isVisible, isFalse); + }); + }); +} diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 7c56fa60e2..464f81d103 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -13,7 +13,9 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -46,6 +48,8 @@ class PresentationContext { currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)), assetServiceProvider.overrideWithValue(service.asset.service), partnerServiceProvider.overrideWithValue(service.partner.service), + remoteAlbumServiceProvider.overrideWithValue(service.album.service), + gCastServiceProvider.overrideWithValue(MockGCastService()), toastRepositoryProvider.overrideWithValue(repository.toast), gCastServiceProvider.overrideWithValue(MockGCastService()), ];