mirror of
https://github.com/immich-app/immich.git
synced 2026-07-09 13:49:37 -07:00
feat: album action
This commit is contained in:
@@ -183,6 +183,12 @@ class RemoteAlbumService {
|
||||
return album.added.length;
|
||||
}
|
||||
|
||||
Future<int> removeAssets({required String albumId, required List<String> 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.
|
||||
|
||||
@@ -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<String> 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<BaseAsset> 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<void> 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));
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<BaseAsset> 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<void> 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);
|
||||
}
|
||||
}
|
||||
-65
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RemoteAlbumBottomSheet>
|
||||
}
|
||||
|
||||
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<RemoteAlbumBottomSheet>
|
||||
],
|
||||
],
|
||||
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)]
|
||||
|
||||
@@ -318,33 +318,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActionResult> 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<ActionResult> 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<ActionResult> updateDescription(ActionSource source, String description) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
if (ids.length != 1) {
|
||||
|
||||
@@ -199,14 +199,6 @@ class ActionService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeFromAlbum(List<String> 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<bool> updateDescription(String assetId, String description) async {
|
||||
// update remote first, then local to ensure consistency
|
||||
await _assetApiRepository.updateDescription(assetId, description);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<MockRemoteAlbumService> {
|
||||
Future<int> Function() get removeAssets =>
|
||||
() => service.removeAssets(
|
||||
albumId: any(named: 'albumId'),
|
||||
assetIds: any(named: 'assetIds'),
|
||||
);
|
||||
|
||||
Future<RemoteAlbum> Function() get updateAlbum =>
|
||||
() => service.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId'));
|
||||
}
|
||||
|
||||
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
||||
Future<List<HashResult>> Function() get hashAssets =>
|
||||
() => api.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'));
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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()),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user