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/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 = ''; @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final recipesAsync = ref.watch(recipeListProvider); final user = ref.watch(authProvider).user; return Scaffold( appBar: AppBar( title: Row( children: [ const Icon(Icons.fork_right_rounded, color: AppTheme.primary, size: 24), const SizedBox(width: 8), const Text('ForkFolio'), ], ), 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: 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())); }, ), body: RefreshIndicator( color: AppTheme.primary, onRefresh: () => ref.read(recipeListProvider.notifier).loadRecipes(), child: Column( children: [ // Search Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: TextField( controller: _searchController, onChanged: (val) => setState(() => _searchQuery = val.trim().toLowerCase()), decoration: InputDecoration( hintText: 'Search recipes, ingredients...', 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 Expanded( child: recipesAsync.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: () => 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)); return matchesTitle || matchesIngredient; }).toList(); if (filtered.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.menu_book_rounded, color: AppTheme.textDisabled.withOpacity(0.5), size: 64), const SizedBox(height: 14), Text( _searchQuery.isEmpty ? 'Your Cookbook is Empty' : 'No recipes match "$_searchQuery"', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textMuted), ), const SizedBox(height: 6), const Text( 'Share a link from Instagram or tap the camera icon to scan a recipe.', style: TextStyle(fontSize: 12, color: AppTheme.textDisabled), ), ], ), ); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, index) { final recipe = filtered[index]; return _buildRecipeCard(recipe); }, ); }, ), ), ], ), ), floatingActionButton: FloatingActionButton.extended( backgroundColor: AppTheme.primary, onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const ScannerScreen())); }, icon: const Icon(Icons.camera_enhance_rounded, color: Colors.white), label: const Text('Scan Recipe', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), ), ); } Widget _buildRecipeCard(Recipe recipe) { return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => _openRecipeDetail(recipe), child: Row( children: [ // Thumbnail Container( width: 100, height: 100, 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) : Image.network( recipe.image!, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.restaurant_rounded, color: AppTheme.textDisabled), )) : const Icon(Icons.restaurant_rounded, color: AppTheme.textDisabled, size: 32), ), // Info Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recipe.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppTheme.textMain), ), const SizedBox(height: 8), Row( children: [ if (recipe.calculatedTotalTime.isNotEmpty) ...[ const Icon(Icons.schedule_rounded, size: 14, color: AppTheme.primary), const SizedBox(width: 4), Text(recipe.calculatedTotalTime, style: const TextStyle(fontSize: 12, color: AppTheme.textMuted)), const SizedBox(width: 12), ], if (recipe.servings != null && recipe.servings!.isNotEmpty) ...[ const Icon(Icons.people_outline_rounded, size: 14, color: AppTheme.secondary), const SizedBox(width: 4), Text(recipe.servings!, style: const TextStyle(fontSize: 12, color: AppTheme.textMuted)), ], ], ), ], ), ), ), // Action IconButton( icon: const Icon(Icons.outdoor_grill_rounded, color: AppTheme.primary), tooltip: 'Start Cook Mode', onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => CookModeScreen(recipe: recipe))); }, ), ], ), ), ); } void _openRecipeDetail(Recipe recipe) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => DraggableScrollableSheet( initialChildSize: 0.75, maxChildSize: 0.95, minChildSize: 0.5, builder: (_, scrollController) => Container( decoration: const BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: ListView( controller: scrollController, children: [ Center( child: Container( width: 44, height: 4, decoration: BoxDecoration(color: AppTheme.border, borderRadius: BorderRadius.circular(2)), ), ), const SizedBox(height: 16), Text(recipe.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800)), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ if (recipe.servings != null && recipe.servings!.isNotEmpty) _buildMetaChip(Icons.people_outline_rounded, '${recipe.servings} Servings', AppTheme.secondary), if (recipe.prepTime != null && recipe.prepTime!.isNotEmpty) _buildMetaChip(Icons.schedule_rounded, 'Prep: ${recipe.prepTime}', AppTheme.primary), if (recipe.cookTime != null && recipe.cookTime!.isNotEmpty) _buildMetaChip(Icons.local_fire_department_rounded, 'Cook: ${recipe.cookTime}', Colors.amber), if (recipe.calculatedTotalTime.isNotEmpty) _buildMetaChip(Icons.timer_rounded, 'Total: ${recipe.calculatedTotalTime}', Colors.tealAccent), ], ), const SizedBox(height: 16), 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'), ), ), ), 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), ), ), 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), const Text('Ingredients', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), 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, fontWeight: FontWeight.w800)), 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: [ Text('${entry.key + 1}. ', style: const TextStyle(color: AppTheme.primary, fontWeight: FontWeight.w700)), Expanded(child: Text(entry.value, style: const TextStyle(fontSize: 14, height: 1.4))), ], ), )), ], ), ), ), ); } Widget _buildMetaChip(IconData icon, String label, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(width: 5), Text( label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: color), ), ], ), ); } Future _shareRecipe(Recipe recipe) async { final storage = ref.read(secureStorageProvider); final serverUrl = await storage.getServerUrl() ?? ''; final buffer = StringBuffer(); buffer.writeln('šŸ³ ${recipe.title}'); if (recipe.calculatedTotalTime.isNotEmpty) { buffer.writeln('ā±ļø Total Time: ${recipe.calculatedTotalTime}'); } if (recipe.servings != null && recipe.servings!.isNotEmpty) { buffer.writeln('šŸ‘„ Servings: ${recipe.servings}'); } if (recipe.shareToken != null && recipe.shareToken!.isNotEmpty && serverUrl.isNotEmpty) { final cleanBase = serverUrl.endsWith('/') ? serverUrl.substring(0, serverUrl.length - 1) : serverUrl; buffer.writeln('šŸ”— ForkFolio Link: $cleanBase/share/${recipe.shareToken}'); } else if (recipe.sourceUrl != null && recipe.sourceUrl!.isNotEmpty) { buffer.writeln('šŸ”— Source: ${recipe.sourceUrl}'); } buffer.writeln('\nšŸ›’ Ingredients:'); for (final ing in recipe.ingredients) { buffer.writeln('• $ing'); } buffer.writeln('\nšŸ“‹ Instructions:'); for (var i = 0; i < recipe.instructions.length; i++) { buffer.writeln('${i + 1}. ${recipe.instructions[i]}'); } try { const platform = MethodChannel('com.forkfolio.mobile/native_share'); await platform.invokeMethod('shareText', { 'text': buffer.toString(), 'subject': recipe.title, }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not trigger share: $e')), ); } } } 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 _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), ], ), ), ); } }