import 'dart:typed_data'; import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; import '../../core/logger.dart'; import '../../data/recipes/recipe_service.dart'; import '../../data/recipes/models/bookmark_category_model.dart'; import '../../data/recipes/models/recipe_model.dart'; import '../add_recipe/add_recipe_screen.dart'; import '../photo_gallery/photo_gallery_screen.dart'; import '../navigation/main_navigation_scaffold.dart'; /// Bookmarks screen displaying all bookmark categories and their recipes. class BookmarksScreen extends StatefulWidget { const BookmarksScreen({super.key}); @override State createState() => _BookmarksScreenState(); } class _BookmarksScreenState extends State { List _categories = []; Map> _recipesByCategory = {}; bool _isLoading = false; String? _errorMessage; RecipeService? _recipeService; bool _wasLoggedIn = false; @override void initState() { super.initState(); _checkLoginState(); _initializeService(); } @override void didChangeDependencies() { super.didChangeDependencies(); _checkLoginState(); if (_recipeService != null && _wasLoggedIn) { _loadBookmarks(); } } @override void didUpdateWidget(BookmarksScreen oldWidget) { super.didUpdateWidget(oldWidget); _checkLoginState(); if (_recipeService != null && _wasLoggedIn) { _loadBookmarks(); } } void _checkLoginState() { final sessionService = ServiceLocator.instance.sessionService; final isLoggedIn = sessionService?.isLoggedIn ?? false; if (isLoggedIn != _wasLoggedIn) { _wasLoggedIn = isLoggedIn; if (mounted) { setState(() {}); if (isLoggedIn && _recipeService != null) { _loadBookmarks(); } } } } Future _initializeService() async { try { _recipeService = ServiceLocator.instance.recipeService; if (_recipeService == null) { throw Exception('RecipeService not available in ServiceLocator'); } } catch (e) { Logger.error('Failed to initialize RecipeService', e); if (mounted) { setState(() { _errorMessage = 'Failed to initialize recipe service: $e'; }); } } } Future _loadBookmarks() async { if (_recipeService == null) return; setState(() { _isLoading = true; _errorMessage = null; }); try { final categories = await _recipeService!.getAllBookmarkCategories(); final recipesMap = >{}; for (final category in categories) { final recipes = await _recipeService!.getRecipesByCategory(category.id); recipesMap[category.id] = recipes; } if (mounted) { setState(() { _categories = categories; _recipesByCategory = recipesMap; _isLoading = false; }); } } catch (e) { Logger.error('Failed to load bookmarks', e); if (mounted) { setState(() { _isLoading = false; _errorMessage = 'Failed to load bookmarks: $e'; }); } } } Color _parseColor(String hexColor) { try { return Color(int.parse(hexColor.replaceAll('#', '0xFF'))); } catch (e) { return Colors.blue; } } void _viewRecipe(RecipeModel recipe) { Navigator.push( context, MaterialPageRoute( builder: (context) => AddRecipeScreen( recipe: recipe, viewMode: true, ), ), ).then((_) { // Reload bookmarks when returning from recipe view _loadBookmarks(); }); } void _openPhotoGallery(List imageUrls, int initialIndex) { if (imageUrls.isEmpty) return; Navigator.push( context, MaterialPageRoute( builder: (context) => PhotoGalleryScreen( imageUrls: imageUrls, initialIndex: initialIndex, ), ), ); } @override Widget build(BuildContext context) { final isLoggedIn = ServiceLocator.instance.sessionService?.isLoggedIn ?? false; if (!isLoggedIn) { return Scaffold( appBar: AppBar(title: const Text('Bookmarks')), body: const Center( child: Text('Please log in to view bookmarks'), ), ); } if (_isLoading) { return Scaffold( appBar: AppBar(title: const Text('Bookmarks')), body: const Center(child: CircularProgressIndicator()), ); } if (_errorMessage != null) { return Scaffold( appBar: AppBar(title: const Text('Bookmarks')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_errorMessage!), const SizedBox(height: 16), ElevatedButton( onPressed: _loadBookmarks, child: const Text('Retry'), ), ], ), ), ); } if (_categories.isEmpty) { return Scaffold( appBar: AppBar(title: const Text('Bookmarks')), body: const Center( child: Text( 'No bookmark categories yet.\nBookmark a recipe to get started!', textAlign: TextAlign.center, ), ), ); } return Scaffold( appBar: AppBar( title: const Text('Bookmarks'), actions: [ IconButton( icon: const Icon(Icons.person), tooltip: 'User', onPressed: () { final scaffold = context.findAncestorStateOfType(); scaffold?.navigateToUser(); }, ), ], ), body: RefreshIndicator( onRefresh: _loadBookmarks, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _categories.length, itemBuilder: (context, index) { final category = _categories[index]; final recipes = _recipesByCategory[category.id] ?? []; return Card( margin: const EdgeInsets.only(bottom: 16), child: ExpansionTile( leading: category.color != null ? Container( width: 32, height: 32, decoration: BoxDecoration( color: _parseColor(category.color!), shape: BoxShape.circle, ), ) : const Icon(Icons.bookmark), title: Text( category.name, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text('${recipes.length} recipe(s)'), children: recipes.isEmpty ? [ const ListTile( title: Text('No recipes in this category'), ) ] : recipes.map((recipe) { return _BookmarkRecipeItem( recipe: recipe, onTap: () => _viewRecipe(recipe), onPhotoTap: (index) => _openPhotoGallery( recipe.imageUrls, index, ), ); }).toList(), ), ); }, ), ), ); } } /// Widget for displaying a recipe item in a bookmark category. class _BookmarkRecipeItem extends StatelessWidget { final RecipeModel recipe; final VoidCallback onTap; final ValueChanged onPhotoTap; const _BookmarkRecipeItem({ required this.recipe, required this.onTap, required this.onPhotoTap, }); /// Builds an image widget using ImmichService for authenticated access. Widget _buildRecipeImage(String imageUrl) { final immichService = ServiceLocator.instance.immichService; final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); if (assetIdMatch != null && immichService != null) { final assetId = assetIdMatch.group(1); if (assetId != null) { return FutureBuilder( future: immichService.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: 32), ); } 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: 32), ); }, 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, ), ), ); }, ); } @override Widget build(BuildContext context) { return ListTile( leading: recipe.imageUrls.isNotEmpty ? GestureDetector( onTap: () => onPhotoTap(0), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 60, height: 60, child: _buildRecipeImage(recipe.imageUrls.first), ), ), ) : Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.restaurant_menu), ), title: Text( recipe.title, style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (recipe.description != null && recipe.description!.isNotEmpty) Text( recipe.description!, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ const Icon(Icons.star, size: 16, color: Colors.amber), const SizedBox(width: 4), Text(recipe.rating.toString()), if (recipe.isFavourite) ...[ const SizedBox(width: 12), const Icon(Icons.favorite, size: 16, color: Colors.red), ], ], ), ], ), onTap: onTap, ); } }