mirror of
https://github.com/immich-app/immich.git
synced 2026-02-28 17:49:05 +03:00
feat(mobile): Allow users to set album cover from mobile app (#25515)
* set album cover from asset
* add to correct kebab group
* add to album selection
* add to legacy control bottom bar
* add tests
* format
* analyze
* Revert "add to legacy control bottom bar"
This reverts commit 9d68e12a08.
* remove unnecessary event emission
* lint
* fix tests
* fix: button order and remove unncessary check
---------
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
? [
|
||||
|
||||
@@ -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) {
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||
if (deletedIds.isEmpty) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user