diff --git a/lib/ui/favourites/favourites_screen.dart b/lib/ui/favourites/favourites_screen.dart index 265fb68..d91a565 100644 --- a/lib/ui/favourites/favourites_screen.dart +++ b/lib/ui/favourites/favourites_screen.dart @@ -21,7 +21,7 @@ class _FavouritesScreenState extends State { String? _errorMessage; RecipeService? _recipeService; bool _wasLoggedIn = false; - bool _isCompactMode = false; + bool _isMinimalView = false; @override void initState() { @@ -210,31 +210,16 @@ class _FavouritesScreenState extends State { return Scaffold( appBar: AppBar( title: const Text('Favourites'), + elevation: 0, actions: [ - // View mode toggle icons + // View mode toggle icon IconButton( - icon: Icon( - _isCompactMode ? Icons.view_list : Icons.view_list_outlined, - color: _isCompactMode ? Theme.of(context).primaryColor : Colors.grey, - ), + icon: Icon(_isMinimalView ? Icons.grid_view : Icons.view_list), onPressed: () { setState(() { - _isCompactMode = true; + _isMinimalView = !_isMinimalView; }); }, - tooltip: 'List View', - ), - IconButton( - icon: Icon( - !_isCompactMode ? Icons.grid_view : Icons.grid_view_outlined, - color: !_isCompactMode ? Theme.of(context).primaryColor : Colors.grey, - ), - onPressed: () { - setState(() { - _isCompactMode = false; - }); - }, - tooltip: 'Grid View', ), ], ), @@ -338,17 +323,15 @@ class _FavouritesScreenState extends State { return RefreshIndicator( onRefresh: _loadFavourites, child: ListView.builder( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _recipes.length, itemBuilder: (context, index) { final recipe = _recipes[index]; return _RecipeCard( recipe: recipe, - isCompact: _isCompactMode, + isMinimal: _isMinimalView, onTap: () => _viewRecipe(recipe), - onFavorite: () => _toggleFavorite(recipe), - onEdit: () => _editRecipe(recipe), - onDelete: () => _deleteRecipe(recipe), + onFavouriteToggle: () => _toggleFavorite(recipe), ); }, ), @@ -356,23 +339,18 @@ class _FavouritesScreenState extends State { } } -/// Card widget for displaying a recipe (reused from RecipesScreen). -/// This is a copy of the _RecipeCard from recipes_screen.dart to avoid making it public. +/// Card widget for displaying a recipe in Favourites screen. class _RecipeCard extends StatelessWidget { final RecipeModel recipe; - final bool isCompact; + final bool isMinimal; final VoidCallback onTap; - final VoidCallback onFavorite; - final VoidCallback onEdit; - final VoidCallback onDelete; + final VoidCallback onFavouriteToggle; const _RecipeCard({ required this.recipe, - required this.isCompact, + required this.isMinimal, required this.onTap, - required this.onFavorite, - required this.onEdit, - required this.onDelete, + required this.onFavouriteToggle, }); void _openPhotoGallery(BuildContext context, int initialIndex) { @@ -388,296 +366,133 @@ class _RecipeCard extends StatelessWidget { ); } - /// Builds an image widget using MediaService for authenticated access. - Widget _buildRecipeImage(String imageUrl) { - final mediaService = ServiceLocator.instance.mediaService; - if (mediaService == null) { - return Image.network(imageUrl, fit: BoxFit.cover); - } - - // Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original) - final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); - String? assetId; - - if (assetIdMatch != null) { - assetId = assetIdMatch.group(1); - } else { - // For Blossom URLs, use the full URL - assetId = imageUrl; - } - - if (assetId != null) { - return FutureBuilder( - future: mediaService.fetchImageBytes(assetId, isThumbnail: true), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Container( - color: Colors.grey.shade200, - child: const Center( - child: CircularProgressIndicator(), - ), - ); - } - - if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { - return Container( - color: Colors.grey.shade200, - child: const Icon(Icons.broken_image, size: 48), - ); - } - - return Image.memory( - snapshot.data!, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - ); - }, - ); - } - - return Image.network( - imageUrl, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey.shade200, - child: const Icon(Icons.broken_image, size: 48), - ); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - color: Colors.grey.shade200, - child: Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); - }, - ); - } - - /// Builds a mosaic-style image grid for multiple images. - Widget _buildMosaicImages(BuildContext context, List imageUrls) { - if (imageUrls.isEmpty) return const SizedBox.shrink(); - if (imageUrls.length == 1) { - return ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: AspectRatio( - aspectRatio: 16 / 9, - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 0), - child: _buildRecipeImage(imageUrls.first), - ), - ), - ); - } - - return ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth; - final height = width * 0.6; - - if (imageUrls.length == 2) { - return SizedBox( - height: height, - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 0), - child: ClipRect( - child: _buildRecipeImage(imageUrls[0]), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 1), - child: ClipRect( - child: _buildRecipeImage(imageUrls[1]), - ), - ), - ), - ], - ), - ); - } else if (imageUrls.length == 3) { - return SizedBox( - height: height, - child: Row( - children: [ - Expanded( - flex: 2, - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 0), - child: ClipRect( - child: _buildRecipeImage(imageUrls[0]), - ), - ), - ), - Expanded( - child: Column( - children: [ - Expanded( - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 1), - child: ClipRect( - child: _buildRecipeImage(imageUrls[1]), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => _openPhotoGallery(context, 2), - child: ClipRect( - child: _buildRecipeImage(imageUrls[2]), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } else { - return SizedBox( - height: height, - child: GridView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsets.zero, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 0, - mainAxisSpacing: 0, - childAspectRatio: 1.0, - ), - itemCount: imageUrls.length > 4 ? 4 : imageUrls.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () => _openPhotoGallery(context, index), - child: ClipRect( - child: Stack( - fit: StackFit.expand, - children: [ - _buildRecipeImage(imageUrls[index]), - if (index == 3 && imageUrls.length > 4) - Container( - color: Colors.black.withValues(alpha: 0.5), - child: Center( - child: Text( - '+${imageUrls.length - 4}', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ); - }, - ), - ); - } - }, - ), - ); + Color _getRatingColor(int rating) { + if (rating >= 4) return Colors.green; + if (rating >= 2) return Colors.orange; + return Colors.red; } @override Widget build(BuildContext context) { - if (isCompact) { - return _buildCompactCard(context); + if (isMinimal) { + return _buildMinimalCard(context); } else { - return _buildExpandedCard(context); + return _buildFullCard(context); } } - Widget _buildCompactCard(BuildContext context) { + Widget _buildMinimalCard(BuildContext context) { return Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 1, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(16), child: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ + // Thumbnail (80x80) if (recipe.imageUrls.isNotEmpty) GestureDetector( onTap: () => _openPhotoGallery(context, 0), child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: SizedBox( + borderRadius: BorderRadius.circular(12), + child: Container( width: 80, height: 80, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), child: _buildRecipeImage(recipe.imageUrls.first), ), ), + ) + else + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.restaurant_menu, + color: Colors.grey[400], + size: 32, + ), ), - if (recipe.imageUrls.isNotEmpty) const SizedBox(width: 12), + const SizedBox(width: 16), + // Content section Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ + // Title + Text( + recipe.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + // Icons and rating row Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: Text( - recipe.title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, + // Left side: Favorite icon + InkWell( + onTap: onFavouriteToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + recipe.isFavourite + ? Icons.favorite + : Icons.favorite_border, + color: recipe.isFavourite ? Colors.red : Colors.grey[600], + size: 22, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), - ], - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - icon: Icon( - recipe.isFavourite ? Icons.favorite : Icons.favorite_border, - color: recipe.isFavourite ? Colors.red : Colors.grey, - size: 24, + // Right side: Rating badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, ), - onPressed: onFavorite, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - tooltip: recipe.isFavourite ? 'Remove from favorites' : 'Add to favorites', - ), - const SizedBox(width: 8), - const Icon( - Icons.star, - size: 20, - color: Colors.amber, - ), - const SizedBox(width: 4), - Text( - recipe.rating.toString(), - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + decoration: BoxDecoration( + color: _getRatingColor(recipe.rating).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + size: 18, + color: Colors.amber, + ), + const SizedBox(width: 4), + Text( + recipe.rating.toString(), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: _getRatingColor(recipe.rating), + fontSize: 14, + ), + ), + ], ), ), ], @@ -685,37 +500,6 @@ class _RecipeCard extends StatelessWidget { ], ), ), - PopupMenuButton( - icon: const Icon(Icons.more_vert, size: 20), - itemBuilder: (context) => [ - PopupMenuItem( - child: const Row( - children: [ - Icon(Icons.edit, size: 18), - SizedBox(width: 8), - Text('Edit'), - ], - ), - onTap: () => Future.delayed( - const Duration(milliseconds: 100), - onEdit, - ), - ), - PopupMenuItem( - child: const Row( - children: [ - Icon(Icons.delete, size: 18, color: Colors.red), - SizedBox(width: 8), - Text('Delete', style: TextStyle(color: Colors.red)), - ], - ), - onTap: () => Future.delayed( - const Duration(milliseconds: 100), - onDelete, - ), - ), - ], - ), ], ), ), @@ -723,113 +507,285 @@ class _RecipeCard extends StatelessWidget { ); } - Widget _buildExpandedCard(BuildContext context) { + Widget _buildFullCard(BuildContext context) { + // Show up to 3 images + final imagesToShow = recipe.imageUrls.take(3).toList(); + return Card( - margin: const EdgeInsets.only(bottom: 16), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (recipe.imageUrls.isNotEmpty) - _buildMosaicImages(context, recipe.imageUrls), + // Photo section with divided layout + if (imagesToShow.isNotEmpty) + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + child: _buildPhotoLayout(context, imagesToShow), + ), Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Title row with actions Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( recipe.title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 18, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), - if (recipe.rating > 0) - Row( - children: List.generate(5, (index) { - return Icon( - index < recipe.rating - ? Icons.star - : Icons.star_border, - size: 16, - color: index < recipe.rating - ? Colors.amber - : Colors.grey, - ); - }), - ), + const SizedBox(width: 8), + // Action icons and rating grouped together + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Favorite icon + InkWell( + onTap: onFavouriteToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + recipe.isFavourite + ? Icons.favorite + : Icons.favorite_border, + color: recipe.isFavourite ? Colors.red : Colors.grey[600], + size: 20, + ), + ), + ), + const SizedBox(width: 8), + // Rating badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: _getRatingColor(recipe.rating).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(18), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + size: 16, + color: Colors.amber, + ), + const SizedBox(width: 4), + Text( + recipe.rating.toString(), + style: TextStyle( + color: _getRatingColor(recipe.rating), + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), ], ), + // Description if (recipe.description != null && recipe.description!.isNotEmpty) ...[ - const SizedBox(height: 8), + const SizedBox(height: 6), Text( recipe.description!, - style: TextStyle( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], fontSize: 14, - color: Colors.grey.shade700, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], - if (recipe.tags.isNotEmpty) ...[ - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: recipe.tags.map((tag) { - return Chip( - label: Text( - tag, - style: const TextStyle(fontSize: 12), - ), - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - ); - }).toList(), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildPhotoLayout(BuildContext context, List imagesToShow) { + final imageCount = imagesToShow.length; + + return AspectRatio( + aspectRatio: 2 / 1, // More compact than 16/9 (which is ~1.78/1) + child: Row( + children: [ + if (imageCount == 1) + // Single image: full width + Expanded( + child: _buildImageTile(context, imagesToShow[0], 0, showBorder: false), + ) + else if (imageCount == 2) + // Two images: split 50/50 + ...imagesToShow.asMap().entries.map((entry) { + final index = entry.key; + return Expanded( + child: _buildImageTile( + context, + entry.value, + index, + showBorder: index == 0, + ), + ); + }) + else + // Three images: one large on left, two stacked on right + Expanded( + flex: 2, + child: _buildImageTile( + context, + imagesToShow[0], + 0, + showBorder: false, + ), + ), + if (imageCount == 3) ...[ + const SizedBox(width: 2), + Expanded( + child: Column( + children: [ + Expanded( + child: _buildImageTile( + context, + imagesToShow[1], + 1, + showBorder: true, + ), + ), + const SizedBox(height: 2), + Expanded( + child: _buildImageTile( + context, + imagesToShow[2], + 2, + showBorder: false, ), - ], - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - icon: Icon( - recipe.isFavourite ? Icons.favorite : Icons.favorite_border, - color: recipe.isFavourite ? Colors.red : Colors.grey, - ), - onPressed: onFavorite, - tooltip: recipe.isFavourite ? 'Remove from favorites' : 'Add to favorites', - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: onEdit, - tooltip: 'Edit', - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: onDelete, - tooltip: 'Delete', - color: Colors.red, - ), - ], ), ], ), ), ], - ), + ], ), ); } -} + Widget _buildImageTile(BuildContext context, String imageUrl, int index, {required bool showBorder}) { + return GestureDetector( + onTap: () => _openPhotoGallery(context, index), + child: Container( + decoration: showBorder + ? BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.white.withValues(alpha: 0.3), + width: 2, + ), + ), + ) + : null, + child: _buildRecipeImage(imageUrl), + ), + ); + } + + Widget _buildRecipeImage(String imageUrl) { + final mediaService = ServiceLocator.instance.mediaService; + if (mediaService == null) { + return Image.network(imageUrl, fit: BoxFit.cover); + } + + // Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original) + final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); + String? assetId; + + if (assetIdMatch != null) { + assetId = assetIdMatch.group(1); + } else { + // For Blossom URLs, use the full URL + assetId = imageUrl; + } + + if (assetId != null) { + return FutureBuilder( + future: mediaService.fetchImageBytes(assetId, isThumbnail: true), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Container( + color: Colors.grey.shade200, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + } + + if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { + return Container( + color: Colors.grey.shade200, + child: const Icon(Icons.broken_image, size: 48), + ); + } + return Image.memory( + snapshot.data!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ); + }, + ); + } + + return Image.network( + imageUrl, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey.shade200, + child: const Icon(Icons.broken_image, size: 48), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.grey.shade200, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/recipes/recipes_screen.dart b/lib/ui/recipes/recipes_screen.dart index 8842f92..5cc9303 100644 --- a/lib/ui/recipes/recipes_screen.dart +++ b/lib/ui/recipes/recipes_screen.dart @@ -423,7 +423,7 @@ class _RecipesScreenState extends State { ), ) : ListView.builder( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _filteredRecipes.length, itemBuilder: (context, index) { final recipe = _filteredRecipes[index]; @@ -491,115 +491,144 @@ class _RecipeCard extends StatelessWidget { Widget _buildMinimalCard(BuildContext context) { return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 1, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + elevation: 2, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), child: Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Small thumbnail (60x60) + // Thumbnail (80x80) if (recipe.imageUrls.isNotEmpty) GestureDetector( onTap: () => _openPhotoGallery(context, 0), child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: SizedBox( - width: 60, - height: 60, + borderRadius: BorderRadius.circular(12), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), child: _buildRecipeImage(recipe.imageUrls.first), ), ), ) else Container( - width: 60, - height: 60, + width: 80, + height: 80, decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), - child: const Icon( + child: Icon( Icons.restaurant_menu, - color: Colors.grey, + color: Colors.grey[400], + size: 32, ), ), - const SizedBox(width: 12), - // Content + const SizedBox(width: 16), + // Content section Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - Expanded( - child: Text( - recipe.title, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + // Title + Text( + recipe.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 4), + const SizedBox(height: 8), + // Icons and rating row Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Bookmark icon - FutureBuilder( - future: _isRecipeBookmarked(recipe.id), - builder: (context, snapshot) { - final isBookmarked = snapshot.data ?? false; - return IconButton( - icon: Icon( - isBookmarked - ? Icons.bookmark - : Icons.bookmark_border, - color: isBookmarked ? Colors.blue : Colors.grey, - size: 24, + // Left side: Action icons + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Favorite icon + InkWell( + onTap: onFavouriteToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + recipe.isFavourite + ? Icons.favorite + : Icons.favorite_border, + color: recipe.isFavourite ? Colors.red : Colors.grey[600], + size: 22, + ), ), - onPressed: onBookmarkToggle, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ); - }, + ), + const SizedBox(width: 8), + // Bookmark icon + FutureBuilder( + future: _isRecipeBookmarked(recipe.id), + builder: (context, snapshot) { + final isBookmarked = snapshot.data ?? false; + return InkWell( + onTap: onBookmarkToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + isBookmarked + ? Icons.bookmark + : Icons.bookmark_border, + color: isBookmarked ? Colors.blue : Colors.grey[600], + size: 22, + ), + ), + ); + }, + ), + ], ), - const SizedBox(width: 4), - // Favorite icon - IconButton( - icon: Icon( - recipe.isFavourite - ? Icons.favorite - : Icons.favorite_border, - color: recipe.isFavourite ? Colors.red : Colors.grey, - size: 24, + // Right side: Rating badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, ), - onPressed: onFavouriteToggle, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: 8), - const Icon( - Icons.star, - size: 20, - color: Colors.amber, - ), - const SizedBox(width: 4), - Text( - recipe.rating.toString(), - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + decoration: BoxDecoration( + color: _getRatingColor(recipe.rating).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + size: 18, + color: Colors.amber, + ), + const SizedBox(width: 4), + Text( + recipe.rating.toString(), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: _getRatingColor(recipe.rating), + fontSize: 14, + ), + ), + ], ), ), ], @@ -619,14 +648,14 @@ class _RecipeCard extends StatelessWidget { final imagesToShow = recipe.imageUrls.take(3).toList(); return Card( - margin: const EdgeInsets.only(bottom: 16), - elevation: 1, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + elevation: 2, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -634,98 +663,115 @@ class _RecipeCard extends StatelessWidget { if (imagesToShow.isNotEmpty) ClipRRect( borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), + top: Radius.circular(16), ), child: _buildPhotoLayout(context, imagesToShow), ), Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Title row with actions Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( recipe.title, style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, + fontSize: 18, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), - const SizedBox(width: 12), - // Bookmark icon - FutureBuilder( - future: _isRecipeBookmarked(recipe.id), - builder: (context, snapshot) { - final isBookmarked = snapshot.data ?? false; - return IconButton( - icon: Icon( - isBookmarked - ? Icons.bookmark - : Icons.bookmark_border, - color: isBookmarked ? Colors.blue : Colors.grey[600], - size: 20, - ), - onPressed: onBookmarkToggle, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ); - }, - ), - const SizedBox(width: 4), - // Favorite icon - IconButton( - icon: Icon( - recipe.isFavourite - ? Icons.favorite - : Icons.favorite_border, - color: recipe.isFavourite ? Colors.red : Colors.grey[600], - size: 20, - ), - onPressed: onFavouriteToggle, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: _getRatingColor(recipe.rating).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.star, - size: 16, - color: Colors.amber, - ), - const SizedBox(width: 4), - Text( - recipe.rating.toString(), - style: TextStyle( - color: _getRatingColor(recipe.rating), - fontWeight: FontWeight.bold, + // Action icons and rating grouped together + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Favorite icon + InkWell( + onTap: onFavouriteToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + recipe.isFavourite + ? Icons.favorite + : Icons.favorite_border, + color: recipe.isFavourite ? Colors.red : Colors.grey[600], + size: 20, ), ), - ], - ), + ), + const SizedBox(width: 4), + // Bookmark icon + FutureBuilder( + future: _isRecipeBookmarked(recipe.id), + builder: (context, snapshot) { + final isBookmarked = snapshot.data ?? false; + return InkWell( + onTap: onBookmarkToggle, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + isBookmarked + ? Icons.bookmark + : Icons.bookmark_border, + color: isBookmarked ? Colors.blue : Colors.grey[600], + size: 20, + ), + ), + ); + }, + ), + const SizedBox(width: 8), + // Rating badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: _getRatingColor(recipe.rating).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(18), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.star, + size: 16, + color: Colors.amber, + ), + const SizedBox(width: 4), + Text( + recipe.rating.toString(), + style: TextStyle( + color: _getRatingColor(recipe.rating), + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ], + ), + ), + ], ), ], ), + // Description if (recipe.description != null && recipe.description!.isNotEmpty) ...[ - const SizedBox(height: 8), + const SizedBox(height: 6), Text( recipe.description!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey[600], + fontSize: 14, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -744,7 +790,7 @@ class _RecipeCard extends StatelessWidget { final imageCount = imagesToShow.length; return AspectRatio( - aspectRatio: 16 / 9, + aspectRatio: 2 / 1, // More compact than 16/9 (which is ~1.78/1) child: Row( children: [ if (imageCount == 1)