diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 9a52f28de..ba9ccf2ff 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -171,67 +171,6 @@ class _RemoteAlbumPageState extends ConsumerState { unawaited(context.pushRoute(DriftActivitiesRoute(album: _album))); } - Future showOptionSheet(BuildContext context) async { - final user = ref.watch(currentUserProvider); - final isOwner = user != null ? user.id == _album.ownerId : false; - final canAddPhotos = - await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor; - - unawaited( - showModalBottomSheet( - context: context, - backgroundColor: context.colorScheme.surface, - isScrollControlled: false, - builder: (context) { - return DriftRemoteAlbumOption( - onDeleteAlbum: isOwner - ? () async { - await deleteAlbum(context); - if (context.mounted) { - context.pop(); - } - } - : null, - onAddUsers: isOwner - ? () async { - await addUsers(context); - context.pop(); - } - : null, - onAddPhotos: isOwner || canAddPhotos - ? () async { - await addAssets(context); - context.pop(); - } - : null, - onToggleAlbumOrder: isOwner - ? () async { - await toggleAlbumOrder(); - context.pop(); - } - : null, - onEditAlbum: isOwner - ? () async { - context.pop(); - await showEditTitleAndDescription(context); - } - : null, - onCreateSharedLink: isOwner - ? () async { - context.pop(); - unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))); - } - : null, - onShowOptions: () { - context.pop(); - context.pushRoute(DriftAlbumOptionsRoute(album: _album)); - }, - ); - }, - ), - ); - } - @override Widget build(BuildContext context) { final user = ref.watch(currentUserProvider); @@ -249,8 +188,16 @@ class _RemoteAlbumPageState extends ConsumerState { child: Timeline( appBar: RemoteAlbumSliverAppBar( icon: Icons.photo_album_outlined, - onShowOptions: () => showOptionSheet(context), - onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, + kebabMenu: _AlbumKebabMenu( + album: _album, + onDeleteAlbum: () => deleteAlbum(context), + onAddUsers: () => addUsers(context), + onAddPhotos: () => addAssets(context), + onToggleAlbumOrder: () => toggleAlbumOrder(), + onEditAlbum: () => showEditTitleAndDescription(context), + onCreateSharedLink: () => unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))), + onShowOptions: () => context.pushRoute(DriftAlbumOptionsRoute(album: _album)), + ), onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, onActivity: () => showActivity(context), ), @@ -414,3 +361,77 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { ); } } + +class _AlbumKebabMenu extends ConsumerWidget { + final RemoteAlbum album; + final VoidCallback? onDeleteAlbum; + final VoidCallback? onAddUsers; + final VoidCallback? onAddPhotos; + final VoidCallback? onToggleAlbumOrder; + final VoidCallback? onEditAlbum; + final VoidCallback? onCreateSharedLink; + final VoidCallback? onShowOptions; + + const _AlbumKebabMenu({ + required this.album, + this.onDeleteAlbum, + this.onAddUsers, + this.onAddPhotos, + this.onToggleAlbumOrder, + this.onEditAlbum, + this.onCreateSharedLink, + this.onShowOptions, + }); + + double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { + if (settings?.maxExtent == null || settings?.minExtent == null) { + return 1.0; + } + + final deltaExtent = settings!.maxExtent - settings.minExtent; + if (deltaExtent <= 0.0) { + return 1.0; + } + + return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + final iconColor = Color.lerp(Colors.white, context.primaryColor, scrollProgress); + final iconShadows = [ + if (scrollProgress < 0.95) + Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5)) + else + const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent), + ]; + + final user = ref.watch(currentUserProvider); + final isOwner = user != null && user.id == album.ownerId; + + return FutureBuilder( + future: ref + .read(remoteAlbumServiceProvider) + .getUserRole(album.id, user?.id ?? '') + .then((role) => role == AlbumUserRole.editor), + builder: (context, snapshot) { + final canAddPhotos = snapshot.data ?? false; + + return DriftRemoteAlbumOption( + iconColor: iconColor, + iconShadows: iconShadows, + onDeleteAlbum: isOwner ? onDeleteAlbum : null, + onAddUsers: isOwner ? onAddUsers : null, + onAddPhotos: isOwner || canAddPhotos ? onAddPhotos : null, + onToggleAlbumOrder: isOwner ? onToggleAlbumOrder : null, + onEditAlbum: isOwner ? onEditAlbum : null, + onCreateSharedLink: isOwner ? onCreateSharedLink : null, + onShowOptions: onShowOptions, + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index 675b5bf21..1ca875e48 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -53,7 +53,7 @@ class BaseActionButton extends ConsumerWidget { style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)), leadingIcon: Icon(iconData, color: effectiveIconColor), onPressed: onPressed, - child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)), + child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16, color: iconColor)), ); } diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart index b82d951b6..355e1a01a 100644 --- a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart +++ b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; class DriftRemoteAlbumOption extends ConsumerWidget { const DriftRemoteAlbumOption({ @@ -14,6 +15,8 @@ class DriftRemoteAlbumOption extends ConsumerWidget { this.onToggleAlbumOrder, this.onEditAlbum, this.onShowOptions, + this.iconColor, + this.iconShadows, }); final VoidCallback? onAddPhotos; @@ -24,73 +27,131 @@ class DriftRemoteAlbumOption extends ConsumerWidget { final VoidCallback? onToggleAlbumOrder; final VoidCallback? onEditAlbum; final VoidCallback? onShowOptions; + final Color? iconColor; + final List? iconShadows; @override Widget build(BuildContext context, WidgetRef ref) { - TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); + final theme = context.themeData; + final menuChildren = []; - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: ListView( - shrinkWrap: true, - children: [ - if (onEditAlbum != null) - ListTile( - leading: const Icon(Icons.edit), - title: Text('edit_album'.t(context: context), style: textStyle), - onTap: onEditAlbum, - ), - if (onAddPhotos != null) - ListTile( - leading: const Icon(Icons.add_a_photo), - title: Text('add_photos'.t(context: context), style: textStyle), - onTap: onAddPhotos, - ), - if (onAddUsers != null) - ListTile( - leading: const Icon(Icons.group_add), - title: Text('album_viewer_page_share_add_users'.t(context: context), style: textStyle), - onTap: onAddUsers, - ), - if (onLeaveAlbum != null) - ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: Text('leave_album'.t(context: context), style: textStyle), - onTap: onLeaveAlbum, - ), - if (onToggleAlbumOrder != null) - ListTile( - leading: const Icon(Icons.swap_vert_rounded), - title: Text('change_display_order'.t(context: context), style: textStyle), - onTap: onToggleAlbumOrder, - ), - if (onCreateSharedLink != null) - ListTile( - leading: const Icon(Icons.link), - title: Text('create_shared_link'.t(context: context), style: textStyle), - onTap: onCreateSharedLink, - ), - if (onShowOptions != null) - ListTile( - leading: const Icon(Icons.settings), - title: Text('options'.t(context: context), style: textStyle), - onTap: onShowOptions, - ), - if (onDeleteAlbum != null) ...[ - const Divider(indent: 16, endIndent: 16), - ListTile( - leading: Icon(Icons.delete, color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]), - title: Text( - 'delete_album'.t(context: context), - style: textStyle.copyWith(color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]), - ), - onTap: onDeleteAlbum, - ), - ], - ], + if (onEditAlbum != null) { + menuChildren.add( + BaseActionButton( + label: 'edit_album'.t(context: context), + iconData: Icons.edit, + onPressed: onEditAlbum, + menuItem: true, ), + ); + } + + if (onAddPhotos != null) { + menuChildren.add( + BaseActionButton( + label: 'add_photos'.t(context: context), + iconData: Icons.add_a_photo, + onPressed: onAddPhotos, + menuItem: true, + ), + ); + } + + if (onAddUsers != null) { + menuChildren.add( + BaseActionButton( + label: 'album_viewer_page_share_add_users'.t(context: context), + iconData: Icons.group_add, + onPressed: onAddUsers, + menuItem: true, + ), + ); + } + + if (onLeaveAlbum != null) { + menuChildren.add( + BaseActionButton( + label: 'leave_album'.t(context: context), + iconData: Icons.person_remove_rounded, + onPressed: onLeaveAlbum, + menuItem: true, + ), + ); + } + + if (onToggleAlbumOrder != null) { + menuChildren.add( + BaseActionButton( + label: 'change_display_order'.t(context: context), + iconData: Icons.swap_vert_rounded, + onPressed: onToggleAlbumOrder, + menuItem: true, + ), + ); + } + + if (onCreateSharedLink != null) { + menuChildren.add( + BaseActionButton( + label: 'create_shared_link'.t(context: context), + iconData: Icons.link, + onPressed: onCreateSharedLink, + menuItem: true, + ), + ); + } + + if (onShowOptions != null) { + menuChildren.add( + BaseActionButton( + label: 'options'.t(context: context), + iconData: Icons.settings, + onPressed: onShowOptions, + menuItem: true, + ), + ); + } + + if (onDeleteAlbum != null) { + menuChildren.add(const Divider(height: 1)); + menuChildren.add( + BaseActionButton( + label: 'delete_album'.t(context: context), + iconData: Icons.delete, + iconColor: context.isDarkTheme ? Colors.red[400] : Colors.red[800], + onPressed: onDeleteAlbum, + menuItem: true, + ), + ); + } + + return MenuAnchor( + consumeOutsideTap: true, + style: MenuStyle( + backgroundColor: WidgetStatePropertyAll(theme.scaffoldBackgroundColor), + surfaceTintColor: const WidgetStatePropertyAll(Colors.grey), + elevation: const WidgetStatePropertyAll(4), + shape: const WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + ), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), ), + menuChildren: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 150), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: menuChildren, + ), + ), + ], + builder: (context, controller, child) { + return IconButton( + icon: Icon(Icons.more_vert_rounded, color: iconColor ?? Colors.white, shadows: iconShadows), + onPressed: () => controller.isOpen ? controller.close() : controller.open(), + ); + }, ); } } diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index c486d473b..30eaf4c55 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -24,15 +24,13 @@ class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { const RemoteAlbumSliverAppBar({ super.key, this.icon = Icons.camera, - this.onShowOptions, - this.onToggleAlbumOrder, + required this.kebabMenu, this.onEditTitle, this.onActivity, }); final IconData icon; - final void Function()? onShowOptions; - final void Function()? onToggleAlbumOrder; + final Widget kebabMenu; final void Function()? onEditTitle; final void Function()? onActivity; @@ -91,21 +89,12 @@ class _MesmerizingSliverAppBarState extends ConsumerState context.maybePop(), ), actions: [ - if (widget.onToggleAlbumOrder != null) - IconButton( - icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onToggleAlbumOrder, - ), if (currentAlbum.isActivityEnabled && currentAlbum.isShared) IconButton( icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows), onPressed: widget.onActivity, ), - if (widget.onShowOptions != null) - IconButton( - icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onShowOptions, - ), + widget.kebabMenu, ], title: Builder( builder: (context) { diff --git a/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart b/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart new file mode 100644 index 000000000..1706b4d30 --- /dev/null +++ b/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; + +import '../../../widget_tester_extensions.dart'; + +void main() { + group('DriftRemoteAlbumOption', () { + testWidgets('shows kebab menu icon button', (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + expect(find.byIcon(Icons.more_vert_rounded), findsOneWidget); + }); + + testWidgets('opens menu when icon button is tapped', (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + }); + + testWidgets('shows edit album option when onEditAlbum is provided', + (tester) async { + bool editCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () => editCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + + await tester.tap(find.byIcon(Icons.edit)); + await tester.pumpAndSettle(); + + expect(editCalled, isTrue); + }); + + testWidgets('hides edit album option when onEditAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsNothing); + }); + + testWidgets('shows add photos option when onAddPhotos is provided', + (tester) async { + bool addPhotosCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () => addPhotosCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + + await tester.tap(find.byIcon(Icons.add_a_photo)); + await tester.pumpAndSettle(); + + expect(addPhotosCalled, isTrue); + }); + + testWidgets('hides add photos option when onAddPhotos is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_a_photo), findsNothing); + }); + + testWidgets('shows add users option when onAddUsers is provided', + (tester) async { + bool addUsersCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddUsers: () => addUsersCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.group_add), findsOneWidget); + + await tester.tap(find.byIcon(Icons.group_add)); + await tester.pumpAndSettle(); + + expect(addUsersCalled, isTrue); + }); + + testWidgets('hides add users option when onAddUsers is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.group_add), findsNothing); + }); + + testWidgets('shows leave album option when onLeaveAlbum is provided', + (tester) async { + bool leaveAlbumCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onLeaveAlbum: () => leaveAlbumCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + + await tester.tap(find.byIcon(Icons.person_remove_rounded)); + await tester.pumpAndSettle(); + + expect(leaveAlbumCalled, isTrue); + }); + + testWidgets('hides leave album option when onLeaveAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + }); + + testWidgets( + 'shows toggle album order option when onToggleAlbumOrder is provided', + (tester) async { + bool toggleOrderCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onToggleAlbumOrder: () => toggleOrderCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + + await tester.tap(find.byIcon(Icons.swap_vert_rounded)); + await tester.pumpAndSettle(); + + expect(toggleOrderCalled, isTrue); + }); + + testWidgets('hides toggle album order option when onToggleAlbumOrder is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + }); + + testWidgets( + 'shows create shared link option when onCreateSharedLink is provided', + (tester) async { + bool createSharedLinkCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onCreateSharedLink: () => createSharedLinkCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.link), findsOneWidget); + + await tester.tap(find.byIcon(Icons.link)); + await tester.pumpAndSettle(); + + expect(createSharedLinkCalled, isTrue); + }); + + testWidgets('hides create shared link option when onCreateSharedLink is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.link), findsNothing); + }); + + testWidgets('shows options option when onShowOptions is provided', + (tester) async { + bool showOptionsCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onShowOptions: () => showOptionsCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.settings), findsOneWidget); + + await tester.tap(find.byIcon(Icons.settings)); + await tester.pumpAndSettle(); + + expect(showOptionsCalled, isTrue); + }); + + testWidgets('hides options option when onShowOptions is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.settings), findsNothing); + }); + + testWidgets('shows delete album option when onDeleteAlbum is provided', + (tester) async { + bool deleteAlbumCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onDeleteAlbum: () => deleteAlbumCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.delete), findsOneWidget); + + await tester.tap(find.byIcon(Icons.delete)); + await tester.pumpAndSettle(); + + expect(deleteAlbumCalled, isTrue); + }); + + testWidgets('hides delete album option when onDeleteAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('shows divider before delete album option', (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byType(Divider), findsOneWidget); + }); + + testWidgets('shows all options when all callbacks are provided', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onAddPhotos: () {}, + onAddUsers: () {}, + onLeaveAlbum: () {}, + onToggleAlbumOrder: () {}, + onCreateSharedLink: () {}, + onShowOptions: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + expect(find.byIcon(Icons.group_add), findsOneWidget); + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + expect(find.byIcon(Icons.link), findsOneWidget); + expect(find.byIcon(Icons.settings), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + expect(find.byType(Divider), findsOneWidget); + }); + + testWidgets('shows no options when all callbacks are null', (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.add_a_photo), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.settings), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('uses custom icon color when provided', (tester) async { + const customColor = Colors.red; + + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption( + iconColor: customColor, + ), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.color, equals(customColor)); + }); + + testWidgets('uses default white color when iconColor is null', + (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.color, equals(Colors.white)); + }); + + testWidgets('applies icon shadows when provided', (tester) async { + final shadows = [ + const Shadow(offset: Offset(0, 2), blurRadius: 5, color: Colors.black), + ]; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + iconShadows: shadows, + ), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.shadows, equals(shadows)); + }); + + group('owner vs non-owner scenarios', () { + testWidgets('owner sees all management options', (tester) async { + // Simulating owner scenario - all callbacks provided + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onAddPhotos: () {}, + onAddUsers: () {}, + onToggleAlbumOrder: () {}, + onCreateSharedLink: () {}, + onShowOptions: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Owner should see all management options + expect(find.byIcon(Icons.edit), findsOneWidget); + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + expect(find.byIcon(Icons.group_add), findsOneWidget); + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + expect(find.byIcon(Icons.link), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + // Owner should NOT see leave album + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + }); + + testWidgets('non-owner with editor role sees limited options', + (tester) async { + // Simulating non-owner with editor role - can add photos, show options, leave + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () {}, + onShowOptions: () {}, + onLeaveAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Editor can add photos + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + // Can see options + expect(find.byIcon(Icons.settings), findsOneWidget); + // Can leave album + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + // Cannot see owner-only options + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('non-owner viewer sees minimal options', (tester) async { + // Simulating viewer - can only show options and leave + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onShowOptions: () {}, + onLeaveAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Can see options + expect(find.byIcon(Icons.settings), findsOneWidget); + // Can leave album + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + // Cannot see any other options + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.add_a_photo), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + }); + }); +}