-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
feat(mobile): Allow users to set album cover from mobile app #25515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9cd9d4b
158666d
8ab396d
9d68e12
2193350
cc770a5
fe37c1a
380b72c
1bb5395
50a9d80
53e6db6
052f119
38e8a1b
a5b4a66
120441d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
|
|
@@ -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) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -42,6 +43,7 @@ class ActionButtonContext { | |
| final bool isCasting; | ||
| final TimelineOrigin timelineOrigin; | ||
| final ThemeData? originalTheme; | ||
| final int selectedCount; | ||
|
|
||
| const ActionButtonContext({ | ||
| required this.asset, | ||
|
|
@@ -56,6 +58,7 @@ class ActionButtonContext { | |
| this.isCasting = false, | ||
| this.timelineOrigin = TimelineOrigin.main, | ||
| this.originalTheme, | ||
| this.selectedCount = 1, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| }); | ||
| } | ||
|
|
||
|
|
@@ -65,6 +68,7 @@ enum ActionButtonType { | |
| share, | ||
| shareLink, | ||
| cast, | ||
| setAlbumCover, | ||
| similarPhotos, | ||
| viewInTimeline, | ||
| download, | ||
|
|
@@ -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, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 && // | ||
|
|
@@ -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( | ||
|
|
@@ -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, | ||
|
|
||
There was a problem hiding this comment.
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