Skip to content

Commit 2017308

Browse files
Merge branch 'main' into main
2 parents 79d6c02 + 3c80049 commit 2017308

11 files changed

+169
-107
lines changed

mobile/lib/presentation/pages/drift_activities.page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
3737
child: Scaffold(
3838
appBar: AppBar(
3939
title: Text(album.name),
40-
actions: [const LikeActivityActionButton(menuItem: true)],
40+
actions: [const LikeActivityActionButton(iconOnly: true)],
4141
actionsPadding: const EdgeInsets.only(right: 8),
4242
),
4343
body: activities.widgetWhen(

mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart

Lines changed: 73 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,34 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
2121

2222
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
2323

24-
class AddActionButton extends ConsumerWidget {
24+
class AddActionButton extends ConsumerStatefulWidget {
2525
const AddActionButton({super.key});
2626

27-
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async {
27+
@override
28+
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
29+
}
30+
31+
class _AddActionButtonState extends ConsumerState<AddActionButton> {
32+
void _handleMenuSelection(AddToMenuItem selected) {
33+
switch (selected) {
34+
case AddToMenuItem.album:
35+
_openAlbumSelector();
36+
break;
37+
case AddToMenuItem.archive:
38+
performArchiveAction(context, ref, source: ActionSource.viewer);
39+
break;
40+
case AddToMenuItem.unarchive:
41+
performUnArchiveAction(context, ref, source: ActionSource.viewer);
42+
break;
43+
case AddToMenuItem.lockedFolder:
44+
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
45+
break;
46+
}
47+
}
48+
49+
List<Widget> _buildMenuChildren() {
2850
final asset = ref.read(currentAssetNotifier);
29-
if (asset == null) return;
51+
if (asset == null) return [];
3052

3153
final user = ref.read(currentUserProvider);
3254
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
@@ -35,93 +57,57 @@ class AddActionButton extends ConsumerWidget {
3557
final hasRemote = asset is RemoteAsset;
3658
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
3759
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
38-
final menuItemHeight = 30.0;
39-
40-
final List<PopupMenuEntry<AddToMenuItem>> items = [
41-
PopupMenuItem(
42-
enabled: false,
43-
textStyle: context.textTheme.labelMedium,
44-
height: 40,
45-
child: Text("add_to_bottom_bar".tr()),
60+
61+
return [
62+
Padding(
63+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
64+
child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
4665
),
47-
PopupMenuItem(
48-
height: menuItemHeight,
49-
value: AddToMenuItem.album,
50-
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())),
66+
BaseActionButton(
67+
iconData: Icons.photo_album_outlined,
68+
label: "album".tr(),
69+
menuItem: true,
70+
onPressed: () => _handleMenuSelection(AddToMenuItem.album),
5171
),
52-
const PopupMenuDivider(),
53-
PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())),
72+
5473
if (isOwner) ...[
74+
const PopupMenuDivider(),
75+
Padding(
76+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
77+
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
78+
),
5579
if (showArchive)
56-
PopupMenuItem(
57-
height: menuItemHeight,
58-
value: AddToMenuItem.archive,
59-
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())),
80+
BaseActionButton(
81+
iconData: Icons.archive_outlined,
82+
label: "archive".tr(),
83+
menuItem: true,
84+
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
6085
),
6186
if (showUnarchive)
62-
PopupMenuItem(
63-
height: menuItemHeight,
64-
value: AddToMenuItem.unarchive,
65-
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())),
87+
BaseActionButton(
88+
iconData: Icons.unarchive_outlined,
89+
label: "unarchive".tr(),
90+
menuItem: true,
91+
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
6692
),
67-
PopupMenuItem(
68-
height: menuItemHeight,
69-
value: AddToMenuItem.lockedFolder,
70-
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())),
93+
BaseActionButton(
94+
iconData: Icons.lock_outline,
95+
label: "locked_folder".tr(),
96+
menuItem: true,
97+
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
7198
),
7299
],
73100
];
74-
75-
final AddToMenuItem? selected = await showMenu<AddToMenuItem>(
76-
context: context,
77-
color: context.themeData.scaffoldBackgroundColor,
78-
position: _menuPosition(context),
79-
items: items,
80-
popUpAnimationStyle: AnimationStyle.noAnimation,
81-
);
82-
83-
if (selected == null) {
84-
return;
85-
}
86-
87-
switch (selected) {
88-
case AddToMenuItem.album:
89-
_openAlbumSelector(context, ref);
90-
break;
91-
case AddToMenuItem.archive:
92-
await performArchiveAction(context, ref, source: ActionSource.viewer);
93-
break;
94-
case AddToMenuItem.unarchive:
95-
await performUnArchiveAction(context, ref, source: ActionSource.viewer);
96-
break;
97-
case AddToMenuItem.lockedFolder:
98-
await performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
99-
break;
100-
}
101101
}
102102

103-
RelativeRect _menuPosition(BuildContext context) {
104-
final renderObject = context.findRenderObject();
105-
if (renderObject is! RenderBox) {
106-
return RelativeRect.fill;
107-
}
108-
109-
final size = renderObject.size;
110-
final position = renderObject.localToGlobal(Offset.zero);
111-
112-
return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy);
113-
}
114-
115-
void _openAlbumSelector(BuildContext context, WidgetRef ref) {
103+
void _openAlbumSelector() {
116104
final currentAsset = ref.read(currentAssetNotifier);
117105
if (currentAsset == null) {
118106
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
119107
return;
120108
}
121109

122-
final List<Widget> slivers = [
123-
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
124-
];
110+
final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
125111

126112
showModalBottomSheet(
127113
context: context,
@@ -141,7 +127,7 @@ class AddActionButton extends ConsumerWidget {
141127
);
142128
}
143129

144-
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async {
130+
Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
145131
final latest = ref.read(currentAssetNotifier);
146132

147133
if (latest == null) {
@@ -174,17 +160,27 @@ class AddActionButton extends ConsumerWidget {
174160
}
175161

176162
@override
177-
Widget build(BuildContext context, WidgetRef ref) {
163+
Widget build(BuildContext context) {
178164
final asset = ref.watch(currentAssetNotifier);
179165
if (asset == null) {
180166
return const SizedBox.shrink();
181167
}
182-
return Builder(
183-
builder: (buttonContext) {
168+
169+
return MenuAnchor(
170+
consumeOutsideTap: true,
171+
style: MenuStyle(
172+
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
173+
elevation: const WidgetStatePropertyAll(4),
174+
shape: const WidgetStatePropertyAll(
175+
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
176+
),
177+
),
178+
menuChildren: _buildMenuChildren(),
179+
builder: (context, controller, child) {
184180
return BaseActionButton(
185181
iconData: Icons.add,
186182
label: "add_to_bottom_bar".tr(),
187-
onPressed: () => _showAddOptions(buttonContext, ref),
183+
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
188184
);
189185
},
190186
);

mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class BaseActionButton extends StatelessWidget {
1111
this.onLongPressed,
1212
this.maxWidth = 90.0,
1313
this.minWidth,
14+
this.iconOnly = false,
1415
this.menuItem = false,
1516
});
1617

@@ -19,6 +20,11 @@ class BaseActionButton extends StatelessWidget {
1920
final Color? iconColor;
2021
final double maxWidth;
2122
final double? minWidth;
23+
24+
/// When true, renders only an IconButton without text label
25+
final bool iconOnly;
26+
27+
/// When true, renders as a MenuItemButton for use in MenuAnchor menus
2228
final bool menuItem;
2329
final void Function()? onPressed;
2430
final void Function()? onLongPressed;
@@ -31,13 +37,26 @@ class BaseActionButton extends StatelessWidget {
3137
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
3238
final textColor = context.themeData.textTheme.labelLarge?.color;
3339

34-
if (menuItem) {
40+
if (iconOnly) {
3541
return IconButton(
3642
onPressed: onPressed,
3743
icon: Icon(iconData, size: iconSize, color: iconColor),
3844
);
3945
}
4046

47+
if (menuItem) {
48+
final theme = context.themeData;
49+
final effectiveStyle = theme.textTheme.labelLarge;
50+
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
51+
52+
return MenuItemButton(
53+
style: MenuItemButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12)),
54+
leadingIcon: Icon(iconData, color: effectiveIconColor, size: 20),
55+
onPressed: onPressed,
56+
child: Text(label, style: effectiveStyle),
57+
);
58+
}
59+
4160
return ConstrainedBox(
4261
constraints: BoxConstraints(maxWidth: maxWidth),
4362
child: MaterialButton(

mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
77
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
88

99
class CastActionButton extends ConsumerWidget {
10-
const CastActionButton({super.key, this.menuItem = true});
10+
const CastActionButton({super.key, this.iconOnly = true, this.menuItem = false});
1111

12+
final bool iconOnly;
1213
final bool menuItem;
1314

1415
@override
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
2223
onPressed: () {
2324
showDialog(context: context, builder: (context) => const CastDialog());
2425
},
26+
iconOnly: iconOnly,
2527
menuItem: menuItem,
2628
);
2729
}

mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
1010

1111
class DownloadActionButton extends ConsumerWidget {
1212
final ActionSource source;
13+
final bool iconOnly;
1314
final bool menuItem;
14-
const DownloadActionButton({super.key, required this.source, this.menuItem = false});
15+
const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
1516

1617
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
1718
if (!context.mounted) {
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
3839
iconData: Icons.download,
3940
maxWidth: 95,
4041
label: "download".t(context: context),
42+
iconOnly: iconOnly,
4143
menuItem: menuItem,
4244
onPressed: () => _onTap(context, ref, backgroundManager),
4345
);

mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
1010

1111
class FavoriteActionButton extends ConsumerWidget {
1212
final ActionSource source;
13+
final bool iconOnly;
1314
final bool menuItem;
1415

15-
const FavoriteActionButton({super.key, required this.source, this.menuItem = false});
16+
const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
1617

1718
void _onTap(BuildContext context, WidgetRef ref) async {
1819
if (!context.mounted) {
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
4445
return BaseActionButton(
4546
iconData: Icons.favorite_border_rounded,
4647
label: "favorite".t(context: context),
48+
iconOnly: iconOnly,
4749
menuItem: menuItem,
4850
onPressed: () => _onTap(context, ref),
4951
);

mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
1212
import 'package:immich_mobile/providers/user.provider.dart';
1313

1414
class LikeActivityActionButton extends ConsumerWidget {
15-
const LikeActivityActionButton({super.key, this.menuItem = false});
15+
const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
1616

17+
final bool iconOnly;
1718
final bool menuItem;
1819

1920
@override
@@ -49,6 +50,7 @@ class LikeActivityActionButton extends ConsumerWidget {
4950
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
5051
label: "like".t(context: context),
5152
onPressed: () => onTap(liked),
53+
iconOnly: iconOnly,
5254
menuItem: menuItem,
5355
);
5456
},
@@ -57,6 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
5759
loading: () => BaseActionButton(
5860
iconData: Icons.favorite_border,
5961
label: "like".t(context: context),
62+
iconOnly: iconOnly,
6063
menuItem: menuItem,
6164
),
6265
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),

mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
55
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
66

77
class MotionPhotoActionButton extends ConsumerWidget {
8-
const MotionPhotoActionButton({super.key, this.menuItem = true});
8+
const MotionPhotoActionButton({super.key, this.iconOnly = true, this.menuItem = false});
99

10+
final bool iconOnly;
1011
final bool menuItem;
1112

1213
@override
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
1718
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
1819
label: "play_motion_photo".t(context: context),
1920
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
21+
iconOnly: iconOnly,
2022
menuItem: menuItem,
2123
);
2224
}

mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
1010

1111
class UnFavoriteActionButton extends ConsumerWidget {
1212
final ActionSource source;
13+
final bool iconOnly;
1314
final bool menuItem;
1415

15-
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false});
16+
const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
1617

1718
void _onTap(BuildContext context, WidgetRef ref) async {
1819
if (!context.mounted) {
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
4546
iconData: Icons.favorite_rounded,
4647
label: "unfavorite".t(context: context),
4748
onPressed: () => _onTap(context, ref),
49+
iconOnly: iconOnly,
4850
menuItem: menuItem,
4951
);
5052
}

0 commit comments

Comments
 (0)