mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +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)));
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
@@ -249,8 +188,16 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||
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<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)),
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Shadow>? 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 = <Widget>[];
|
||||
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user