import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/constants/app_theme.dart'; import '../../core/models/recipe.dart'; import '../../core/models/shopping_list.dart'; import '../../core/providers.dart'; import '../cook_mode/cook_mode_screen.dart'; import '../editor/recipe_edit_screen.dart'; import '../scanner/scanner_screen.dart'; class RecipesScreen extends ConsumerStatefulWidget { const RecipesScreen({super.key}); @override ConsumerState createState() => _RecipesScreenState(); } class _RecipesScreenState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; static const MethodChannel _shareChannel = MethodChannel('com.forkfolio.mobile/native_share'); int _selectedTab = 0; // 0: My Cookbook, 1: Community Feed @override void dispose() { _searchController.dispose(); super.dispose(); } Future _shareRecipe(Recipe recipe) async { final buffer = StringBuffer(); buffer.writeln('${recipe.title}\n'); if (recipe.description.isNotEmpty) { buffer.writeln('${recipe.description}\n'); } if (recipe.ingredients.isNotEmpty) { buffer.writeln('Ingredients:'); for (final ing in recipe.ingredients) { buffer.writeln('• $ing'); } buffer.writeln(); } if (recipe.instructions.isNotEmpty) { buffer.writeln('Instructions:'); for (var i = 0; i < recipe.instructions.length; i++) { buffer.writeln('${i + 1}. ${recipe.instructions[i]}'); } buffer.writeln(); } if (recipe.shareToken != null && recipe.shareToken!.isNotEmpty) { final storage = ref.read(secureStorageProvider); final serverUrl = await storage.getServerUrl(); if (serverUrl != null) { buffer.writeln('View online: $serverUrl/recipes/share/${recipe.shareToken}'); } } try { await _shareChannel.invokeMethod('shareText', { 'title': recipe.title, 'text': buffer.toString().trim(), }); } on MissingPluginException { await Clipboard.setData(ClipboardData(text: buffer.toString().trim())); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( backgroundColor: AppTheme.surfaceElevated, content: Text('Recipe text copied to clipboard!'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.danger, content: Text('Failed to share: $e'), ), ); } } } Future _cloneRecipe(Recipe recipe) async { if (recipe.id == null) return; try { final cloned = await ref.read(publicRecipesProvider.notifier).cloneRecipe(recipe.id!, ref); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.surfaceElevated, content: Text('Cloned "${cloned.title}" to your private library!'), action: SnackBarAction( label: 'VIEW', textColor: AppTheme.primary, onPressed: () { setState(() => _selectedTab = 0); }, ), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.danger, content: Text('Failed to clone recipe: $e'), ), ); } } } @override Widget build(BuildContext context) { final recipesAsync = ref.watch(recipeListProvider); final publicRecipesAsync = ref.watch(publicRecipesProvider); final user = ref.watch(authProvider).user; final currentUsername = user?['username']?.toString().toLowerCase(); return Scaffold( appBar: AppBar( title: const Row( children: [ Icon(Icons.fork_right_rounded, color: AppTheme.primary, size: 24), SizedBox(width: 8), Text('ForkFolio', style: TextStyle(fontWeight: FontWeight.w700)), ], ), actions: [ IconButton( tooltip: 'Camera Scanner', icon: const Icon(Icons.camera_alt_outlined), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const ScannerScreen())); }, ), IconButton( tooltip: 'Profile & Settings', icon: const Icon(Icons.account_circle_outlined), onPressed: _showProfileDialog, ), ], ), floatingActionButton: _selectedTab == 0 ? FloatingActionButton.extended( backgroundColor: AppTheme.primary, foregroundColor: Colors.black, icon: const Icon(Icons.add_rounded), label: const Text('New Recipe', style: TextStyle(fontWeight: FontWeight.w700)), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const RecipeEditScreen())); }, ) : null, body: RefreshIndicator( color: AppTheme.primary, onRefresh: () async { if (_selectedTab == 0) { await ref.read(recipeListProvider.notifier).loadRecipes(); } else { await ref.read(publicRecipesProvider.notifier).loadPublicRecipes(); } }, child: Column( children: [ // Tab Selector: My Cookbook vs Community Feed Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), child: Row( children: [ Expanded( child: ChoiceChip( selected: _selectedTab == 0, label: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.bookmark_outline_rounded, size: 16), SizedBox(width: 6), Text('My Cookbook', style: TextStyle(fontWeight: FontWeight.w600)), ], ), selectedColor: AppTheme.primary, backgroundColor: AppTheme.surfaceElevated, side: BorderSide(color: _selectedTab == 0 ? AppTheme.primary : AppTheme.border), labelStyle: TextStyle( color: _selectedTab == 0 ? Colors.black : Colors.white70, ), onSelected: (_) => setState(() => _selectedTab = 0), ), ), const SizedBox(width: 10), Expanded( child: ChoiceChip( selected: _selectedTab == 1, label: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.explore_outlined, size: 16), SizedBox(width: 6), Text('Community Feed', style: TextStyle(fontWeight: FontWeight.w600)), ], ), selectedColor: AppTheme.primary, backgroundColor: AppTheme.surfaceElevated, side: BorderSide(color: _selectedTab == 1 ? AppTheme.primary : AppTheme.border), labelStyle: TextStyle( color: _selectedTab == 1 ? Colors.black : Colors.white70, ), onSelected: (_) => setState(() => _selectedTab = 1), ), ), ], ), ), // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: TextField( controller: _searchController, onChanged: (val) => setState(() => _searchQuery = val.trim().toLowerCase()), decoration: InputDecoration( hintText: _selectedTab == 0 ? 'Search my recipes, ingredients...' : 'Search community recipes...', prefixIcon: const Icon(Icons.search_rounded, size: 20), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear_rounded, size: 18), onPressed: () { _searchController.clear(); setState(() => _searchQuery = ''); }, ) : null, ), ), ), // Recipe List (Active Tab) Expanded( child: _selectedTab == 0 ? _buildRecipeList(recipesAsync, isCommunity: false, currentUsername: currentUsername) : _buildRecipeList(publicRecipesAsync, isCommunity: true, currentUsername: currentUsername), ), ], ), ), ); } Widget _buildRecipeList( AsyncValue> stateAsync, { required bool isCommunity, required String? currentUsername, }) { return stateAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: AppTheme.primary), ), error: (err, _) => Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off_rounded, color: AppTheme.textMuted, size: 48), const SizedBox(height: 12), Text( 'Could not load recipes: $err', textAlign: TextAlign.center, style: const TextStyle(color: AppTheme.textMuted), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { if (isCommunity) { ref.read(publicRecipesProvider.notifier).loadPublicRecipes(); } else { ref.read(recipeListProvider.notifier).loadRecipes(); } }, icon: const Icon(Icons.refresh_rounded), label: const Text('Retry'), ), ], ), ), ), data: (recipes) { final filtered = recipes.where((r) { if (_searchQuery.isEmpty) return true; final matchesTitle = r.title.toLowerCase().contains(_searchQuery); final matchesIngredient = r.ingredients.any((i) => i.toLowerCase().contains(_searchQuery)); final matchesAuthor = (r.createdBy ?? '').toLowerCase().contains(_searchQuery); return matchesTitle || matchesIngredient || matchesAuthor; }).toList(); if (filtered.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isCommunity ? Icons.explore_off_outlined : Icons.menu_book_rounded, color: AppTheme.textDisabled.withOpacity(0.5), size: 64, ), const SizedBox(height: 14), Text( _searchQuery.isEmpty ? (isCommunity ? 'No Public Recipes Found' : 'Your Cookbook is Empty') : 'No recipes match "$_searchQuery"', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textMuted), ), const SizedBox(height: 8), if (!isCommunity && _searchQuery.isEmpty) ElevatedButton.icon( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const RecipeEditScreen())); }, icon: const Icon(Icons.add), label: const Text('Add Your First Recipe'), ), ], ), ); } return ListView.separated( padding: const EdgeInsets.all(16), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, index) { final recipe = filtered[index]; final isOwnRecipe = currentUsername != null && recipe.createdBy != null && recipe.createdBy!.toLowerCase() == currentUsername; return Card( child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => _openRecipeDetail(recipe, isCommunity: isCommunity, isOwnRecipe: isOwnRecipe), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ // Recipe Thumbnail ClipRRect( borderRadius: BorderRadius.circular(10), child: Container( width: 80, height: 80, color: AppTheme.surfaceElevated, child: recipe.image != null && recipe.image!.isNotEmpty ? (recipe.image!.startsWith('data:image') ? Image.memory( base64Decode(recipe.image!.split(',').last), fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.restaurant, color: AppTheme.textMuted), ) : Image.network( recipe.image!, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.restaurant, color: AppTheme.textMuted), )) : const Icon(Icons.restaurant, color: AppTheme.textMuted), ), ), const SizedBox(width: 14), // Recipe Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recipe.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), const SizedBox(height: 4), if (isCommunity && recipe.createdBy != null && recipe.createdBy!.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ const Icon(Icons.person_pin_rounded, size: 13, color: AppTheme.secondary), const SizedBox(width: 4), Text( '@${recipe.createdBy}', style: const TextStyle(fontSize: 12, color: AppTheme.secondary, fontWeight: FontWeight.w600), ), ], ), ), Text( recipe.description.isNotEmpty ? recipe.description : '${recipe.ingredients.length} ingredients', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, color: AppTheme.textMuted), ), const SizedBox(height: 6), Row( children: [ if (recipe.calculatedTotalTime.isNotEmpty) ...[ const Icon(Icons.timer_outlined, size: 14, color: AppTheme.secondary), const SizedBox(width: 4), Text( recipe.calculatedTotalTime, style: const TextStyle(fontSize: 12, color: AppTheme.secondary, fontWeight: FontWeight.w600), ), const SizedBox(width: 12), ], const Icon(Icons.format_list_bulleted_rounded, size: 14, color: AppTheme.textMuted), const SizedBox(width: 4), Text( '${recipe.ingredients.length} items', style: const TextStyle(fontSize: 12, color: AppTheme.textMuted), ), ], ), ], ), ), // Action icon if (isCommunity && !isOwnRecipe) IconButton( tooltip: 'Clone to My Library', icon: const Icon(Icons.bookmark_add_outlined, color: AppTheme.primary), onPressed: () => _cloneRecipe(recipe), ) else const Icon(Icons.chevron_right_rounded, color: AppTheme.textMuted), ], ), ), ), ); }, ); }, ); } void _openRecipeDetail(Recipe recipe, {bool isCommunity = false, bool isOwnRecipe = true}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.5, maxChildSize: 0.95, builder: (_, scrollController) => Container( decoration: const BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: ListView( controller: scrollController, padding: const EdgeInsets.all(20), children: [ Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: AppTheme.border, borderRadius: BorderRadius.circular(2), ), ), ), // Header image if present if (recipe.image != null && recipe.image!.isNotEmpty) ...[ ClipRRect( borderRadius: BorderRadius.circular(16), child: recipe.image!.startsWith('data:image') ? Image.memory( base64Decode(recipe.image!.split(',').last), height: 200, width: double.infinity, fit: BoxFit.cover, ) : Image.network( recipe.image!, height: 200, width: double.infinity, fit: BoxFit.cover, ), ), const SizedBox(height: 16), ], Text( recipe.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800), ), if (isCommunity && recipe.createdBy != null) ...[ const SizedBox(height: 6), Row( children: [ const Icon(Icons.person_outline, size: 15, color: AppTheme.secondary), const SizedBox(width: 4), Text( 'Shared by @${recipe.createdBy}', style: const TextStyle(fontSize: 13, color: AppTheme.secondary, fontWeight: FontWeight.w600), ), ], ), ], if (recipe.description.isNotEmpty) ...[ const SizedBox(height: 8), Text( recipe.description, style: const TextStyle(fontSize: 14, color: AppTheme.textMuted), ), ], const SizedBox(height: 16), // Timing & servings metadata badges Wrap( spacing: 8, runSpacing: 8, children: [ if (recipe.servings.isNotEmpty) _buildMetaChip(Icons.people_outline, 'Servings: ${recipe.servings}'), if (recipe.prepTime.isNotEmpty) _buildMetaChip(Icons.kitchen_outlined, 'Prep: ${recipe.prepTime}'), if (recipe.cookTime.isNotEmpty) _buildMetaChip(Icons.timer_outlined, 'Cook: ${recipe.cookTime}'), if (recipe.calculatedTotalTime.isNotEmpty) _buildMetaChip(Icons.schedule_rounded, 'Total: ${recipe.calculatedTotalTime}', isHighlighted: true), ], ), const SizedBox(height: 16), // Main Action Buttons Row( children: [ Expanded( child: SizedBox( height: 48, child: ElevatedButton.icon( onPressed: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => CookModeScreen(recipe: recipe))); }, icon: const Icon(Icons.outdoor_grill_rounded), label: const Text('Cook Mode'), ), ), ), if (isCommunity && !isOwnRecipe) ...[ const SizedBox(width: 8), Container( height: 48, decoration: BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.primary), ), child: IconButton( tooltip: 'Clone to My Library', icon: const Icon(Icons.bookmark_add_rounded, color: AppTheme.primary, size: 22), onPressed: () { Navigator.pop(ctx); _cloneRecipe(recipe); }, ), ), ], if (!isCommunity || isOwnRecipe) ...[ const SizedBox(width: 8), Container( height: 48, width: 48, decoration: BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.border), ), child: IconButton( tooltip: 'Edit Recipe', icon: const Icon(Icons.edit_outlined, color: AppTheme.secondary, size: 20), onPressed: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => RecipeEditScreen(recipe: recipe))); }, ), ), ], const SizedBox(width: 8), Container( height: 48, width: 48, decoration: BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.border), ), child: IconButton( tooltip: 'Share Recipe', icon: const Icon(Icons.share_rounded, color: AppTheme.primary, size: 20), onPressed: () => _shareRecipe(recipe), ), ), if (!isCommunity || isOwnRecipe) ...[ const SizedBox(width: 8), Container( height: 48, width: 48, decoration: BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.border), ), child: IconButton( tooltip: 'Delete Recipe', icon: const Icon(Icons.delete_outline_rounded, color: AppTheme.danger, size: 20), onPressed: () => _confirmDelete(ctx, recipe), ), ), ], ], ), const SizedBox(height: 24), // Ingredients Header with "Add to Shopping List" Row( children: [ const Text('Ingredients', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), const Spacer(), if (recipe.ingredients.isNotEmpty) TextButton.icon( style: TextButton.styleFrom( visualDensity: VisualDensity.compact, foregroundColor: AppTheme.secondary, ), icon: const Icon(Icons.add_shopping_cart_rounded, size: 16), label: const Text('Add to List', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)), onPressed: () => _showAddToShoppingListSheet(ctx, recipe), ), ], ), const SizedBox(height: 8), ...recipe.ingredients.map((ing) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('• ', style: TextStyle(color: AppTheme.primary, fontSize: 16)), Expanded(child: Text(ing, style: const TextStyle(fontSize: 14))), ], ), )), const SizedBox(height: 24), const Text('Instructions', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), const SizedBox(height: 8), ...recipe.instructions.asMap().entries.map((entry) => Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 10, backgroundColor: AppTheme.surfaceElevated, child: Text( '${entry.key + 1}', style: const TextStyle(fontSize: 10, color: AppTheme.primary, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 10), Expanded(child: Text(entry.value, style: const TextStyle(fontSize: 14, height: 1.4))), ], ), )), ], ), ), ), ); } Widget _buildMetaChip(IconData icon, String label, {bool isHighlighted = false}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: isHighlighted ? AppTheme.primary.withOpacity(0.15) : AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(8), border: Border.all(color: isHighlighted ? AppTheme.primary : AppTheme.border), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: isHighlighted ? AppTheme.primary : AppTheme.textMuted), const SizedBox(width: 5), Text( label, style: TextStyle( fontSize: 12, fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w500, color: isHighlighted ? AppTheme.primary : Colors.white, ), ), ], ), ); } Future _confirmDelete(BuildContext sheetCtx, Recipe recipe) async { final confirmed = await showDialog( context: context, builder: (dlgCtx) => AlertDialog( backgroundColor: AppTheme.surfaceElevated, title: const Text('Delete Recipe?'), content: Text('Are you sure you want to delete "${recipe.title}"? This cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(dlgCtx, false), child: const Text('Cancel', style: TextStyle(color: AppTheme.textMuted)), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.danger), onPressed: () => Navigator.pop(dlgCtx, true), child: const Text('Delete', style: TextStyle(color: Colors.white)), ), ], ), ); if (confirmed == true && recipe.id != null) { Navigator.pop(sheetCtx); try { await ref.read(recipeListProvider.notifier).remove(recipe.id!); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.surfaceElevated, content: Text('Deleted "${recipe.title}"'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.danger, content: Text('Failed to delete recipe: $e'), ), ); } } } } void _showAddToShoppingListSheet(BuildContext parentSheetCtx, Recipe recipe) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (modalCtx) => _AddToShoppingListModal(recipe: recipe), ); } void _showProfileDialog() async { final user = ref.read(authProvider).user; final storage = ref.read(secureStorageProvider); final serverUrl = await storage.getServerUrl(); final username = user?['username'] ?? 'User'; final role = user?['isAdmin'] == true ? 'Administrator' : (user?['isRecipeManager'] == true ? 'Recipe Manager' : 'Member'); if (!mounted) return; showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (ctx) => Container( padding: const EdgeInsets.all(24), decoration: const BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( radius: 24, backgroundColor: AppTheme.primary.withOpacity(0.2), child: Text( username.isNotEmpty ? username[0].toUpperCase() : 'U', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.primary), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(username, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text(role, style: const TextStyle(fontSize: 13, color: AppTheme.secondary, fontWeight: FontWeight.w600)), ], ), ), ], ), const SizedBox(height: 20), const Divider(color: AppTheme.border), const SizedBox(height: 12), Row( children: [ const Icon(Icons.dns_rounded, size: 16, color: AppTheme.textMuted), const SizedBox(width: 8), Expanded( child: Text( serverUrl ?? 'Connected Server', style: const TextStyle(fontSize: 13, color: AppTheme.textMuted), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 48, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: AppTheme.danger, side: const BorderSide(color: AppTheme.danger), ), icon: const Icon(Icons.logout_rounded), label: const Text('Log Out'), onPressed: () { Navigator.pop(ctx); ref.read(authProvider.notifier).logout(); }, ), ), const SizedBox(height: 12), ], ), ), ); } } class _AddToShoppingListModal extends ConsumerStatefulWidget { final Recipe recipe; const _AddToShoppingListModal({required this.recipe}); @override ConsumerState<_AddToShoppingListModal> createState() => _AddToShoppingListModalState(); } class _AddToShoppingListModalState extends ConsumerState<_AddToShoppingListModal> { late final Set _selectedIndices; String? _chosenListId; bool _isSubmitting = false; @override void initState() { super.initState(); _selectedIndices = Set.from( List.generate(widget.recipe.ingredients.length, (i) => i), ); } @override Widget build(BuildContext context) { final listsAsync = ref.watch(shoppingListsProvider); return Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.8, ), decoration: const BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: AppTheme.border, borderRadius: BorderRadius.circular(2), ), ), ), Row( children: [ const Icon(Icons.add_shopping_cart_rounded, color: AppTheme.primary, size: 22), const SizedBox(width: 8), const Expanded( child: Text( 'Add to Shopping List', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), ), ), IconButton( icon: const Icon(Icons.close, size: 20), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 12), // Destination List Selector listsAsync.when( loading: () => const LinearProgressIndicator(color: AppTheme.primary), error: (err, _) => Text('Failed to load lists: $err', style: const TextStyle(color: AppTheme.danger)), data: (lists) { if (lists.isEmpty) { return const Text('No shopping lists available.'); } if (_chosenListId == null || !lists.any((l) => l.id == _chosenListId)) { _chosenListId = lists.first.id; } return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppTheme.border), ), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: _chosenListId, dropdownColor: AppTheme.surfaceElevated, items: lists.map((l) { return DropdownMenuItem( value: l.id, child: Text('Add to: ${l.name} (${l.items.length} items)'), ); }).toList(), onChanged: (val) { if (val != null) setState(() => _chosenListId = val); }, ), ), ); }, ), const SizedBox(height: 14), // Select All / Deselect All Row( children: [ Text( '${_selectedIndices.length} of ${widget.recipe.ingredients.length} selected', style: const TextStyle(fontSize: 13, color: AppTheme.textMuted), ), const Spacer(), TextButton( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), onPressed: () { setState(() { if (_selectedIndices.length == widget.recipe.ingredients.length) { _selectedIndices.clear(); } else { _selectedIndices.addAll(List.generate(widget.recipe.ingredients.length, (i) => i)); } }); }, child: Text( _selectedIndices.length == widget.recipe.ingredients.length ? 'Deselect All' : 'Select All', style: const TextStyle(fontSize: 12, color: AppTheme.primary), ), ), ], ), const Divider(color: AppTheme.border, height: 1), // Checkable Ingredients List Expanded( child: ListView.builder( itemCount: widget.recipe.ingredients.length, itemBuilder: (ctx, i) { final ing = widget.recipe.ingredients[i]; final isSelected = _selectedIndices.contains(i); return CheckboxListTile( value: isSelected, activeColor: AppTheme.primary, checkColor: Colors.black, dense: true, contentPadding: EdgeInsets.zero, title: Text( ing, style: TextStyle( fontSize: 14, color: isSelected ? Colors.white : AppTheme.textMuted, ), ), onChanged: (val) { setState(() { if (val == true) { _selectedIndices.add(i); } else { _selectedIndices.remove(i); } }); }, ); }, ), ), const SizedBox(height: 12), // Submit Button SizedBox( width: double.infinity, height: 48, child: ElevatedButton.icon( onPressed: (_selectedIndices.isEmpty || _chosenListId == null || _isSubmitting) ? null : () async { setState(() => _isSubmitting = true); try { final itemsToAdd = _selectedIndices.map((i) => widget.recipe.ingredients[i]).toList(); await ref.read(shoppingListsProvider.notifier).addItems( _chosenListId!, itemsToAdd, recipeId: widget.recipe.id, recipeTitle: widget.recipe.title, ); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.surfaceElevated, content: Text('Added ${itemsToAdd.length} ingredients to your shopping list!'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: AppTheme.danger, content: Text('Failed to add items: $e'), ), ); } } finally { if (mounted) { setState(() => _isSubmitting = false); } } }, icon: _isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black), ) : const Icon(Icons.check_rounded), label: Text('Add ${_selectedIndices.length} Ingredients to List'), ), ), ], ), ); } }