mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
feat(mobile): album options to kebab menu (#24204)
* feat(mobile): refactor album options into kebab menu for improved UX * feat(mobile): update BaseActionButton to use iconColor for text styling and add delete button color in DriftRemoteAlbumOption * feat: const Divider(height: 1) * fix(mobile): update icon color for album options menu button * chore: refactor * chore: refactor * add test --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -171,67 +171,6 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
unawaited(context.pushRoute(DriftActivitiesRoute(album: _album)));
|
unawaited(context.pushRoute(DriftActivitiesRoute(album: _album)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
@@ -249,8 +188,16 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
child: Timeline(
|
child: Timeline(
|
||||||
appBar: RemoteAlbumSliverAppBar(
|
appBar: RemoteAlbumSliverAppBar(
|
||||||
icon: Icons.photo_album_outlined,
|
icon: Icons.photo_album_outlined,
|
||||||
onShowOptions: () => showOptionSheet(context),
|
kebabMenu: _AlbumKebabMenu(
|
||||||
onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null,
|
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,
|
onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null,
|
||||||
onActivity: () => showActivity(context),
|
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<FlexibleSpaceBarSettings>();
|
||||||
|
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<bool>(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class BaseActionButton extends ConsumerWidget {
|
|||||||
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
||||||
leadingIcon: Icon(iconData, color: effectiveIconColor),
|
leadingIcon: Icon(iconData, color: effectiveIconColor),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
|
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16, color: iconColor)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_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 {
|
class DriftRemoteAlbumOption extends ConsumerWidget {
|
||||||
const DriftRemoteAlbumOption({
|
const DriftRemoteAlbumOption({
|
||||||
@@ -14,6 +15,8 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
this.onToggleAlbumOrder,
|
this.onToggleAlbumOrder,
|
||||||
this.onEditAlbum,
|
this.onEditAlbum,
|
||||||
this.onShowOptions,
|
this.onShowOptions,
|
||||||
|
this.iconColor,
|
||||||
|
this.iconShadows,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onAddPhotos;
|
final VoidCallback? onAddPhotos;
|
||||||
@@ -24,73 +27,131 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
final VoidCallback? onToggleAlbumOrder;
|
final VoidCallback? onToggleAlbumOrder;
|
||||||
final VoidCallback? onEditAlbum;
|
final VoidCallback? onEditAlbum;
|
||||||
final VoidCallback? onShowOptions;
|
final VoidCallback? onShowOptions;
|
||||||
|
final Color? iconColor;
|
||||||
|
final List<Shadow>? iconShadows;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600);
|
final theme = context.themeData;
|
||||||
|
final menuChildren = <Widget>[];
|
||||||
|
|
||||||
return SafeArea(
|
if (onEditAlbum != null) {
|
||||||
child: Padding(
|
menuChildren.add(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
BaseActionButton(
|
||||||
child: ListView(
|
label: 'edit_album'.t(context: context),
|
||||||
shrinkWrap: true,
|
iconData: Icons.edit,
|
||||||
children: [
|
onPressed: onEditAlbum,
|
||||||
if (onEditAlbum != null)
|
menuItem: true,
|
||||||
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 (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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,13 @@ class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
|||||||
const RemoteAlbumSliverAppBar({
|
const RemoteAlbumSliverAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
this.icon = Icons.camera,
|
this.icon = Icons.camera,
|
||||||
this.onShowOptions,
|
required this.kebabMenu,
|
||||||
this.onToggleAlbumOrder,
|
|
||||||
this.onEditTitle,
|
this.onEditTitle,
|
||||||
this.onActivity,
|
this.onActivity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final void Function()? onShowOptions;
|
final Widget kebabMenu;
|
||||||
final void Function()? onToggleAlbumOrder;
|
|
||||||
final void Function()? onEditTitle;
|
final void Function()? onEditTitle;
|
||||||
final void Function()? onActivity;
|
final void Function()? onActivity;
|
||||||
|
|
||||||
@@ -91,21 +89,12 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
|||||||
onPressed: () => context.maybePop(),
|
onPressed: () => context.maybePop(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (widget.onToggleAlbumOrder != null)
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows),
|
|
||||||
onPressed: widget.onToggleAlbumOrder,
|
|
||||||
),
|
|
||||||
if (currentAlbum.isActivityEnabled && currentAlbum.isShared)
|
if (currentAlbum.isActivityEnabled && currentAlbum.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows),
|
icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows),
|
||||||
onPressed: widget.onActivity,
|
onPressed: widget.onActivity,
|
||||||
),
|
),
|
||||||
if (widget.onShowOptions != null)
|
widget.kebabMenu,
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
|
||||||
onPressed: widget.onShowOptions,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
title: Builder(
|
title: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
|||||||
@@ -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<IconButton>(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<IconButton>(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<IconButton>(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user