Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same syntax as the other actions

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/stack_action_button.widget.dart';
Expand Down Expand Up @@ -113,6 +114,8 @@ 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),
],
slivers: ownsAlbum
? [
Expand Down
16 changes: 16 additions & 0 deletions mobile/lib/providers/infrastructure/action.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,22 @@ class ActionNotifier extends Notifier<void> {
}
}

Future<ActionResult> setAlbumCover(ActionSource source, String albumId) async {
final assets = _getAssets(source);
final asset = assets.first;
if (asset is! RemoteAsset) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we need/want to gate here. might be unnecessary since we gate in mobile/lib/utils/action_button.utils.dart already.

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) {
Expand Down
6 changes: 6 additions & 0 deletions mobile/lib/services/action.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ class ActionService {
return _downloadRepository.downloadAllAssets(assets);
}

Future<bool> setAlbumCover(String albumId, String assetId) async {
final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, thumbnailAssetId: assetId);
await _remoteAlbumRepository.update(updatedAlbum);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making sure the local drift db stays in sync

return true;
}

Future<int> _deleteLocalAssets(List<String> localIds) async {
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
if (deletedIds.isEmpty) {
Expand Down
17 changes: 16 additions & 1 deletion mobile/lib/utils/action_button.utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_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/remove_from_lock_folder_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/similar_photos_action_button.widget.dart';
Expand All @@ -42,6 +43,7 @@ class ActionButtonContext {
final bool isCasting;
final TimelineOrigin timelineOrigin;
final ThemeData? originalTheme;
final int selectedCount;

const ActionButtonContext({
required this.asset,
Expand All @@ -56,6 +58,7 @@ class ActionButtonContext {
this.isCasting = false,
this.timelineOrigin = TimelineOrigin.main,
this.originalTheme,
this.selectedCount = 1,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introduced a new class parameter with default 1 in the constructor as that reflects the current usage of this class

});
}

Expand All @@ -65,6 +68,7 @@ enum ActionButtonType {
share,
shareLink,
cast,
setAlbumCover,
similarPhotos,
viewInTimeline,
download,
Expand Down Expand Up @@ -134,6 +138,11 @@ enum ActionButtonType {
context.isOwner && //
!context.isInLockedView && //
context.currentAlbum != null,
ActionButtonType.setAlbumCover =>
context.isOwner && //
!context.isInLockedView && //
context.currentAlbum != null && //
context.selectedCount == 1,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only show when exactly one asset is selected

ActionButtonType.unstack =>
context.isOwner && //
!context.isInLockedView && //
Expand Down Expand Up @@ -213,6 +222,12 @@ enum ActionButtonType {
iconOnly: iconOnly,
menuItem: menuItem,
),
ActionButtonType.setAlbumCover => SetAlbumCoverActionButton(
albumId: context.currentAlbum!.id,
source: context.source,
iconOnly: iconOnly,
menuItem: menuItem,
),
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
Expand Down Expand Up @@ -251,7 +266,7 @@ enum ActionButtonType {
int get kebabMenuGroup => switch (this) {
// 0: info
ActionButtonType.openInfo => 0,
// 10: move,remove, and delete
// 10: move, remove, and delete
ActionButtonType.trash => 10,
ActionButtonType.deletePermanent => 10,
ActionButtonType.removeFromLockFolder => 10,
Expand Down
124 changes: 124 additions & 0 deletions mobile/test/utils/action_button_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,115 @@ void main() {
});
});

group('setAlbumCover button', () {
test('should show when owner, not locked, has album, and selectedCount is 1', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 1,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isTrue);
});

test('should not show when not owner', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: false,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 1,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse);
});

test('should not show when in locked view', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: true,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 1,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse);
});

test('should not show when no current album', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 1,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse);
});

test('should not show when selectedCount is not 1', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 0,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse);
});

test('should not show when selectedCount is greater than 1', () {
final album = createRemoteAlbum();
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
selectedCount: 2,
);

expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse);
});
});

group('likeActivity button', () {
test('should show when not locked, has album, activity enabled, and shared', () {
final album = createRemoteAlbum(isActivityEnabled: true, isShared: true);
Expand Down Expand Up @@ -846,6 +955,21 @@ void main() {
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.setAlbumCover) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.unstack) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
Expand Down
Loading