From eafc267e92b275796e1edb746c3c3b9dc75925e7 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Fri, 15 May 2026 22:14:43 +0300 Subject: [PATCH] feat: improve ui for larger devices --- lib/views/checklists/checklist_item_tile.dart | 235 ++++++++---------- lib/views/home/home_view.dart | 180 +++++++++----- lib/widgets/context_menu_region.dart | 41 +++ lib/widgets/folder_tile.dart | 225 +++++++++-------- lib/widgets/note_tile.dart | 105 ++++---- lib/widgets/photo_tile.dart | 168 +++++++------ macos/Runner/MainFlutterWindow.swift | 7 + 7 files changed, 535 insertions(+), 426 deletions(-) create mode 100644 lib/widgets/context_menu_region.dart diff --git a/lib/views/checklists/checklist_item_tile.dart b/lib/views/checklists/checklist_item_tile.dart index 834bdfb..4367fe0 100644 --- a/lib/views/checklists/checklist_item_tile.dart +++ b/lib/views/checklists/checklist_item_tile.dart @@ -8,6 +8,7 @@ import 'package:pantry/services/checklist_service.dart'; import 'package:pantry/services/prefs_service.dart'; import 'package:pantry/utils/category_icons.dart'; import 'package:pantry/utils/rrule.dart'; +import 'package:pantry/widgets/context_menu_region.dart'; import 'package:pantry/widgets/image_preview.dart'; import 'package:provider/provider.dart'; @@ -45,68 +46,77 @@ class ChecklistItemTile extends StatelessWidget { type: MaterialType.transparency, child: Opacity( opacity: dimmed ? 0.5 : 1.0, - child: InkWell( - onTap: tapRowToToggle ? () => onToggle(item) : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - children: [ - Checkbox(value: item.done, onChanged: (_) => onToggle(item)), - if (item.imageFileId != null) ...[ - GestureDetector( - onTap: () => _showImagePreview(context), - child: Hero( - tag: 'item-image-${item.id}', - child: _ItemImage( - houseId: houseId, - fileId: item.imageFileId!, - owner: item.imageUploadedBy ?? '', + child: ContextMenuRegion( + itemBuilder: _menuItems, + onSelected: (value) => _onMenuSelected(value), + child: InkWell( + onTap: tapRowToToggle ? () => onToggle(item) : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: Row( + children: [ + Checkbox(value: item.done, onChanged: (_) => onToggle(item)), + if (item.imageFileId != null) ...[ + GestureDetector( + onTap: () => _showImagePreview(context), + child: Hero( + tag: 'item-image-${item.id}', + child: _ItemImage( + houseId: houseId, + fileId: item.imageFileId!, + owner: item.imageUploadedBy ?? '', + ), ), ), - ), - const SizedBox(width: 8), - ], - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.name, - style: theme.textTheme.bodyLarge?.copyWith( - decoration: dimmed - ? TextDecoration.lineThrough - : null, - ), - ), - if (_hasBadges) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Wrap( - spacing: 4, - runSpacing: 4, - children: _buildBadges(context), + const SizedBox(width: 8), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + style: theme.textTheme.bodyLarge?.copyWith( + decoration: dimmed + ? TextDecoration.lineThrough + : null, ), ), - ], + if (_hasBadges) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: _buildBadges(context), + ), + ), + ], + ), ), - ), - IconButton( - icon: Icon( - Icons.visibility_outlined, - size: 20, - color: theme.colorScheme.onSurfaceVariant, + IconButton( + icon: Icon( + Icons.visibility_outlined, + size: 20, + color: theme.colorScheme.onSurfaceVariant, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => onView(item), ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () => onView(item), - ), - _MoreMenuButton( - item: item, - onEdit: onEdit, - onMove: onMove, - onDelete: onDelete, - ), - ], + PopupMenuButton( + icon: Icon( + Icons.more_horiz, + size: 20, + color: theme.colorScheme.onSurfaceVariant, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + itemBuilder: (_) => _menuItems(), + onSelected: _onMenuSelected, + ), + ], + ), ), ), ), @@ -114,6 +124,50 @@ class ChecklistItemTile extends StatelessWidget { ); } + List> _menuItems() => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit, size: 18), + const SizedBox(width: 8), + Text(m.checklists.editItem), + ], + ), + ), + PopupMenuItem( + value: 'move', + child: Row( + children: [ + const Icon(Icons.drive_file_move_outlined, size: 18), + const SizedBox(width: 8), + Text(m.checklists.moveItem), + ], + ), + ), + PopupMenuItem( + value: 'remove', + child: Row( + children: [ + const Icon(Icons.delete, size: 18), + const SizedBox(width: 8), + Text(m.checklists.removeItem), + ], + ), + ), + ]; + + void _onMenuSelected(String value) { + switch (value) { + case 'edit': + onEdit(item); + case 'move': + onMove(item); + case 'remove': + onDelete(item); + } + } + void _showImagePreview(BuildContext context) { final uri = ChecklistService.instance.itemImagePreviewUri( houseId, @@ -260,72 +314,3 @@ class _Badge extends StatelessWidget { ); } } - -class _MoreMenuButton extends StatelessWidget { - final ListItem item; - final ValueChanged onEdit; - final ValueChanged onMove; - final ValueChanged onDelete; - - const _MoreMenuButton({ - required this.item, - required this.onEdit, - required this.onMove, - required this.onDelete, - }); - - @override - Widget build(BuildContext context) { - return PopupMenuButton( - icon: Icon( - Icons.more_horiz, - size: 20, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - itemBuilder: (context) => [ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit, size: 18), - const SizedBox(width: 8), - Text(m.checklists.editItem), - ], - ), - ), - PopupMenuItem( - value: 'move', - child: Row( - children: [ - const Icon(Icons.drive_file_move_outlined, size: 18), - const SizedBox(width: 8), - Text(m.checklists.moveItem), - ], - ), - ), - PopupMenuItem( - value: 'remove', - child: Row( - children: [ - const Icon(Icons.delete, size: 18), - const SizedBox(width: 8), - Text(m.checklists.removeItem), - ], - ), - ), - ], - onSelected: (value) { - switch (value) { - case 'edit': - onEdit(item); - case 'move': - onMove(item); - case 'remove': - onDelete(item); - } - }, - ); - } -} diff --git a/lib/views/home/home_view.dart b/lib/views/home/home_view.dart index a0383a8..6474af6 100644 --- a/lib/views/home/home_view.dart +++ b/lib/views/home/home_view.dart @@ -1,3 +1,6 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -68,6 +71,8 @@ class _HomeViewBodyState extends State<_HomeViewBody> final _pageController = PageController(); final _notificationsController = NotificationsController(); + static bool get _isMacOS => !kIsWeb && Platform.isMacOS; + @override void initState() { super.initState(); @@ -119,11 +124,15 @@ class _HomeViewBodyState extends State<_HomeViewBody> void _goToTab(int index) { if (index == _tabIndex) return; - _pageController.animateToPage( - index, - duration: const Duration(milliseconds: 280), - curve: Curves.easeInOut, - ); + if (_pageController.hasClients) { + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 280), + curve: Curves.easeInOut, + ); + } else { + setState(() => _tabIndex = index); + } } void _consumePendingDeepLink() { @@ -161,65 +170,118 @@ class _HomeViewBodyState extends State<_HomeViewBody> @override Widget build(BuildContext context) { final controller = context.watch(); - final houseId = controller.currentHouse?.id; + final destinations = <_NavDestination>[ + (icon: Icons.assignment_turned_in, label: m.nav.checklists), + (icon: Icons.photo, label: m.nav.photoBoard), + (icon: Icons.insert_drive_file, label: m.nav.notesWall), + ]; - return Scaffold( - appBar: AppBar( - title: Text(_tabTitle), - actions: [ - if (_tabIndex == 0 && houseId != null) - IconButton( - icon: const Icon(Icons.sell_outlined), - tooltip: m.checklists.categories, - onPressed: () { + return LayoutBuilder( + builder: (context, constraints) { + final useRail = constraints.maxWidth >= 720; + final extendedRail = constraints.maxWidth >= 1100; + final body = _buildBody(controller, useRail: useRail); + + final appBar = AppBar( + title: Text(_tabTitle), + actions: [ + if (_tabIndex == 0 && houseId != null) + IconButton( + icon: const Icon(Icons.sell_outlined), + tooltip: m.checklists.categories, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CategoriesView(houseId: houseId), + ), + ); + }, + ), + NotificationsBell( + controller: _notificationsController, + onTap: () { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => CategoriesView(houseId: houseId), + builder: (_) => + NotificationsView(controller: _notificationsController), ), ); }, ), - NotificationsBell( - controller: _notificationsController, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - NotificationsView(controller: _notificationsController), - ), - ); - }, + UserMenuButton( + houses: controller.houses, + currentHouse: controller.currentHouse, + onHouseSelected: controller.selectHouse, + onCreateHouse: () => showCreateHouseDialog(context, controller), + onOpenSettings: () { + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => const SettingsView())); + }, + onLogout: widget.onLogout, + ), + ], + ); + + if (useRail) { + return Scaffold( + body: SafeArea( + child: Row( + children: [ + NavigationRail( + extended: extendedRail, + selectedIndex: _tabIndex, + onDestinationSelected: _goToTab, + labelType: extendedRail + ? NavigationRailLabelType.none + : NavigationRailLabelType.all, + leading: _isMacOS ? const SizedBox(height: 24) : null, + destinations: [ + for (final d in destinations) + NavigationRailDestination( + icon: Icon(d.icon), + label: Text(d.label), + ), + ], + ), + const VerticalDivider(width: 1, thickness: 1), + Expanded( + child: Column( + children: [ + appBar, + Expanded( + child: Padding( + padding: EdgeInsetsDirectional.only( + start: _tabIndex == 0 ? 0 : 16, + ), + child: body, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + return Scaffold( + appBar: appBar, + body: body, + bottomNavigationBar: _AnimatedBottomNav( + pageController: _pageController, + currentIndex: _tabIndex, + onTap: _goToTab, + destinations: destinations, ), - UserMenuButton( - houses: controller.houses, - currentHouse: controller.currentHouse, - onHouseSelected: controller.selectHouse, - onCreateHouse: () => showCreateHouseDialog(context, controller), - onOpenSettings: () { - Navigator.of( - context, - ).push(MaterialPageRoute(builder: (_) => const SettingsView())); - }, - onLogout: widget.onLogout, - ), - ], - ), - body: _buildBody(controller), - bottomNavigationBar: _AnimatedBottomNav( - pageController: _pageController, - currentIndex: _tabIndex, - onTap: _goToTab, - destinations: [ - (icon: Icons.assignment_turned_in, label: m.nav.checklists), - (icon: Icons.photo, label: m.nav.photoBoard), - (icon: Icons.insert_drive_file, label: m.nav.notesWall), - ], - ), + ); + }, ); } - Widget _buildBody(HomeController controller) { + Widget _buildBody(HomeController controller, {required bool useRail}) { if (controller.isLoading) { return const Center(child: CircularProgressIndicator()); } @@ -252,15 +314,19 @@ class _HomeViewBodyState extends State<_HomeViewBody> } final houseId = controller.currentHouse!.id; + final pages = [ + ChecklistsView(key: ValueKey('checklists-$houseId'), houseId: houseId), + PhotoBoardView(key: ValueKey('photos-$houseId'), houseId: houseId), + NotesWallView(key: ValueKey('notes-$houseId'), houseId: houseId), + ]; + if (useRail) { + return IndexedStack(index: _tabIndex, children: pages); + } return PageView( controller: _pageController, physics: const ClampingScrollPhysics(), onPageChanged: (i) => setState(() => _tabIndex = i), - children: [ - ChecklistsView(key: ValueKey('checklists-$houseId'), houseId: houseId), - PhotoBoardView(key: ValueKey('photos-$houseId'), houseId: houseId), - NotesWallView(key: ValueKey('notes-$houseId'), houseId: houseId), - ], + children: pages, ); } } diff --git a/lib/widgets/context_menu_region.dart b/lib/widgets/context_menu_region.dart new file mode 100644 index 0000000..c5b2156 --- /dev/null +++ b/lib/widgets/context_menu_region.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +/// Wraps [child] so that a secondary-tap (right-click on desktop / web) +/// opens [items] as a popup menu anchored at the pointer position. +/// +/// Pair with a [PopupMenuButton] / [TileMenuButton] using the same +/// items + onSelected so touch and pointer users get the same affordance. +class ContextMenuRegion extends StatelessWidget { + final List> Function() itemBuilder; + final ValueChanged onSelected; + final Widget child; + + const ContextMenuRegion({ + super.key, + required this.itemBuilder, + required this.onSelected, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onSecondaryTapDown: (details) => _show(context, details.globalPosition), + child: child, + ); + } + + Future _show(BuildContext context, Offset globalPosition) async { + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final value = await showMenu( + context: context, + position: RelativeRect.fromRect( + Rect.fromPoints(globalPosition, globalPosition), + Offset.zero & overlay.size, + ), + items: itemBuilder(), + ); + if (value != null) onSelected(value); + } +} diff --git a/lib/widgets/folder_tile.dart b/lib/widgets/folder_tile.dart index 1f0aa73..dab90fa 100644 --- a/lib/widgets/folder_tile.dart +++ b/lib/widgets/folder_tile.dart @@ -6,6 +6,7 @@ import 'package:pantry/models/photo.dart'; import 'package:pantry/services/auth_service.dart'; import 'package:pantry/services/photo_service.dart'; import 'package:pantry/views/photos/photo_board_controller.dart'; +import 'package:pantry/widgets/context_menu_region.dart'; import 'package:pantry/widgets/tile_menu_button.dart'; class FolderTile extends StatelessWidget { @@ -38,128 +39,104 @@ class FolderTile extends StatelessWidget { }, builder: (context, candidateData, rejectedData) { final isHovering = candidateData.isNotEmpty; - return GestureDetector( - onTap: () => controller.enterFolder(folder.id), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: isHovering - ? Border.all(color: theme.colorScheme.primary, width: 2) - : null, - ), - child: Stack( - children: [ - // Photo stack or folder icon — full bleed - Positioned.fill( - child: Padding( - padding: const EdgeInsets.all(8), - child: previewPhotos.isNotEmpty - ? _buildPhotoStack(theme, headers) - : Center( - child: Icon( - Icons.folder, - size: 56, - color: theme.colorScheme.onSurfaceVariant, + return ContextMenuRegion( + itemBuilder: _menuItems, + onSelected: (value) => _onMenuSelected(context, value), + child: GestureDetector( + onTap: () => controller.enterFolder(folder.id), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: isHovering + ? Border.all(color: theme.colorScheme.primary, width: 2) + : null, + ), + child: Stack( + children: [ + // Photo stack or folder icon — full bleed + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(8), + child: previewPhotos.isNotEmpty + ? _buildPhotoStack(theme, headers) + : Center( + child: Icon( + Icons.folder, + size: 56, + color: theme.colorScheme.onSurfaceVariant, + ), ), - ), + ), ), - ), - // Count badge - if (photoCount > 0) + // Count badge + if (photoCount > 0) + Positioned( + top: 6, + left: 6, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: theme.colorScheme.inverseSurface, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + m.photoBoard.photoCount(photoCount), + style: TextStyle( + fontSize: 11, + color: theme.colorScheme.onInverseSurface, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + // Folder name with gradient Positioned( - top: 6, - left: 6, + left: 0, + right: 0, + bottom: 0, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, + padding: const EdgeInsets.only( + left: 6, + right: 6, + bottom: 6, + top: 20, ), decoration: BoxDecoration( - color: theme.colorScheme.inverseSurface, - borderRadius: BorderRadius.circular(10), + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withAlpha(180), + Colors.transparent, + ], + ), ), child: Text( - m.photoBoard.photoCount(photoCount), - style: TextStyle( - fontSize: 11, - color: theme.colorScheme.onInverseSurface, - fontWeight: FontWeight.bold, + folder.name, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ), ), ), - // Folder name with gradient - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - padding: const EdgeInsets.only( - left: 6, - right: 6, - bottom: 6, - top: 20, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withAlpha(180), - Colors.transparent, - ], - ), - ), - child: Text( - folder.name, - style: theme.textTheme.bodySmall?.copyWith( - color: Colors.white, - ), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, + // Menu + Positioned( + top: 2, + right: 2, + child: TileMenuButton( + items: _menuItems(), + onSelected: (value) => _onMenuSelected(context, value), ), ), - ), - // Menu - Positioned( - top: 2, - right: 2, - child: TileMenuButton( - items: [ - PopupMenuItem( - value: 'rename', - child: Row( - children: [ - const Icon(Icons.edit, size: 18), - const SizedBox(width: 8), - Text(m.photoBoard.renameFolder), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - const Icon(Icons.delete, size: 18), - const SizedBox(width: 8), - Text(m.photoBoard.deleteFolder), - ], - ), - ), - ], - onSelected: (value) { - switch (value) { - case 'rename': - _renameFolder(context); - case 'delete': - _deleteFolder(context); - } - }, - ), - ), - ], + ], + ), ), ), ); @@ -167,6 +144,38 @@ class FolderTile extends StatelessWidget { ); } + List> _menuItems() => [ + PopupMenuItem( + value: 'rename', + child: Row( + children: [ + const Icon(Icons.edit, size: 18), + const SizedBox(width: 8), + Text(m.photoBoard.renameFolder), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete, size: 18), + const SizedBox(width: 8), + Text(m.photoBoard.deleteFolder), + ], + ), + ), + ]; + + void _onMenuSelected(BuildContext context, String value) { + switch (value) { + case 'rename': + _renameFolder(context); + case 'delete': + _deleteFolder(context); + } + } + Widget _buildPhotoStack(ThemeData theme, Map headers) { // Show up to 3 photos stacked with slight rotations final count = previewPhotos.length; diff --git a/lib/widgets/note_tile.dart b/lib/widgets/note_tile.dart index 6dbc591..c64a3ba 100644 --- a/lib/widgets/note_tile.dart +++ b/lib/widgets/note_tile.dart @@ -8,6 +8,7 @@ import 'package:pantry/utils/text_direction.dart'; import 'package:pantry/views/notes/note_detail_view.dart'; import 'package:pantry/views/notes/note_form_view.dart'; import 'package:pantry/views/notes/notes_controller.dart'; +import 'package:pantry/widgets/context_menu_region.dart'; class NoteTile extends StatelessWidget { final Note note; @@ -28,7 +29,7 @@ class NoteTile extends StatelessWidget { onTap: () => controller.toggleSelection(note.id), child: Stack( children: [ - _buildCard(theme, bgColor, textColor), + _buildCard(context, bgColor, textColor), Positioned( top: 4, left: 4, @@ -59,19 +60,24 @@ class NoteTile extends StatelessWidget { child: SizedBox( width: 160, height: 160, - child: _buildCard(theme, bgColor, textColor), + child: _buildCard(context, bgColor, textColor), ), ), - child: GestureDetector( - onTap: () => _viewNote(context, bgColor, textColor), - child: _buildCard(theme, bgColor, textColor), + child: ContextMenuRegion( + itemBuilder: _menuItems, + onSelected: (value) => _onMenuSelected(context, value), + child: GestureDetector( + onTap: () => _viewNote(context, bgColor, textColor), + child: _buildCard(context, bgColor, textColor), + ), ), ); }, ); } - Widget _buildCard(ThemeData theme, Color bgColor, Color textColor) { + Widget _buildCard(BuildContext context, Color bgColor, Color textColor) { + final theme = Theme.of(context); final titleDir = detectTextDirection(note.title); final contentDir = detectTextDirection(note.content); @@ -102,10 +108,12 @@ class NoteTile extends StatelessWidget { ), ), ), - _NoteMenuButton( - note: note, - controller: controller, - color: textColor, + PopupMenuButton( + icon: Icon(Icons.more_vert, size: 20, color: textColor), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + itemBuilder: (_) => _menuItems(), + onSelected: (value) => _onMenuSelected(context, value), ), ], ), @@ -210,56 +218,37 @@ class NoteTile extends StatelessWidget { final luminance = bg.computeLuminance(); return luminance > 0.5 ? Colors.black87 : Colors.white; } -} -class _NoteMenuButton extends StatelessWidget { - final Note note; - final NotesController controller; - final Color color; + List> _menuItems() => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit, size: 18), + const SizedBox(width: 8), + Text(m.notesWall.editNote), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete, size: 18), + const SizedBox(width: 8), + Text(m.common.delete), + ], + ), + ), + ]; - const _NoteMenuButton({ - required this.note, - required this.controller, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return PopupMenuButton( - icon: Icon(Icons.more_vert, size: 20, color: color), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onSelected: (value) { - switch (value) { - case 'edit': - _editNote(context); - case 'delete': - _confirmDelete(context); - } - }, - itemBuilder: (_) => [ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - const Icon(Icons.edit, size: 18), - const SizedBox(width: 8), - Text(m.notesWall.editNote), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - const Icon(Icons.delete, size: 18), - const SizedBox(width: 8), - Text(m.common.delete), - ], - ), - ), - ], - ); + void _onMenuSelected(BuildContext context, String value) { + switch (value) { + case 'edit': + _editNote(context); + case 'delete': + _confirmDelete(context); + } } void _editNote(BuildContext context) { diff --git a/lib/widgets/photo_tile.dart b/lib/widgets/photo_tile.dart index f034149..fff6444 100644 --- a/lib/widgets/photo_tile.dart +++ b/lib/widgets/photo_tile.dart @@ -7,6 +7,7 @@ import 'package:pantry/services/auth_service.dart'; import 'package:pantry/services/photo_service.dart'; import 'package:pantry/views/photos/photo_board_controller.dart'; import 'package:pantry/views/photos/photo_detail_view.dart'; +import 'package:pantry/widgets/context_menu_region.dart'; import 'package:pantry/widgets/tile_menu_button.dart'; class PhotoTile extends StatelessWidget { @@ -131,91 +132,102 @@ class PhotoTile extends StatelessWidget { Uri uri, Map headers, ) { - return GestureDetector( - onTap: () => _showPhotoDetail(context, uri, headers), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Stack( - fit: StackFit.expand, - children: [ - CachedNetworkImage( - imageUrl: uri.toString(), - httpHeaders: headers, - fit: BoxFit.cover, - errorWidget: (_, _, _) => Container( - color: theme.colorScheme.surfaceContainerHighest, - child: const Icon(Icons.broken_image_outlined, size: 32), - ), - ), - Positioned( - top: 2, - right: 2, - child: TileMenuButton( - items: [ - PopupMenuItem( - value: 'caption', - child: Row( - children: [ - const Icon(Icons.edit, size: 18), - const SizedBox(width: 8), - Text(m.photoBoard.caption), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - const Icon(Icons.delete, size: 18), - const SizedBox(width: 8), - Text(m.common.delete), - ], - ), - ), - ], - onSelected: (value) { - switch (value) { - case 'caption': - _editCaption(context); - case 'delete': - _confirmDelete(context); - } - }, - ), - ), - if (photo.caption != null && photo.caption!.isNotEmpty) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 4, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black.withAlpha(180), Colors.transparent], - ), - ), - child: Text( - photo.caption!, - style: theme.textTheme.bodySmall?.copyWith( - color: Colors.white, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + return ContextMenuRegion( + itemBuilder: _menuItems, + onSelected: (value) => _onMenuSelected(context, value), + child: GestureDetector( + onTap: () => _showPhotoDetail(context, uri, headers), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + fit: StackFit.expand, + children: [ + CachedNetworkImage( + imageUrl: uri.toString(), + httpHeaders: headers, + fit: BoxFit.cover, + errorWidget: (_, _, _) => Container( + color: theme.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.broken_image_outlined, size: 32), ), ), - ], + Positioned( + top: 2, + right: 2, + child: TileMenuButton( + items: _menuItems(), + onSelected: (value) => _onMenuSelected(context, value), + ), + ), + if (photo.caption != null && photo.caption!.isNotEmpty) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 4, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withAlpha(180), + Colors.transparent, + ], + ), + ), + child: Text( + photo.caption!, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), ), ), ); } + List> _menuItems() => [ + PopupMenuItem( + value: 'caption', + child: Row( + children: [ + const Icon(Icons.edit, size: 18), + const SizedBox(width: 8), + Text(m.photoBoard.caption), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete, size: 18), + const SizedBox(width: 8), + Text(m.common.delete), + ], + ), + ), + ]; + + void _onMenuSelected(BuildContext context, String value) { + switch (value) { + case 'caption': + _editCaption(context); + case 'delete': + _confirmDelete(context); + } + } + void _showPhotoDetail( BuildContext context, Uri previewUri, diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 3cc05eb..3729ec1 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -8,6 +8,13 @@ class MainFlutterWindow: NSWindow { self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + // Hide title bar but keep traffic-light buttons; let Flutter content + // draw under the chrome. + self.titleVisibility = .hidden + self.titlebarAppearsTransparent = true + self.styleMask.insert(.fullSizeContentView) + self.isMovableByWindowBackground = true + RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib()