import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; import '../../core/logger.dart'; import '../../data/recipes/recipe_service.dart'; import '../../data/recipes/models/recipe_model.dart'; import '../shared/primary_app_bar.dart'; import '../add_recipe/add_recipe_screen.dart'; import '../navigation/main_navigation_scaffold.dart'; import 'package:video_player/video_player.dart'; /// Home screen showing recipes overview, stats, tags, and favorites. class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { RecipeService? _recipeService; List _allRecipes = []; List _favoriteRecipes = []; List _recentRecipes = []; Map _tagCounts = {}; bool _isLoading = true; @override void initState() { super.initState(); _initializeService(); } Future _initializeService() async { try { _recipeService = ServiceLocator.instance.recipeService; if (_recipeService != null) { await _loadData(); } } catch (e) { Logger.error('Failed to initialize home screen', e); if (mounted) { setState(() { _isLoading = false; }); } } } Future _loadData() async { if (_recipeService == null) return; try { final allRecipes = await _recipeService!.getAllRecipes(); final favorites = await _recipeService!.getFavouriteRecipes(); // Calculate tag counts final tagCounts = {}; for (final recipe in allRecipes) { for (final tag in recipe.tags) { tagCounts[tag] = (tagCounts[tag] ?? 0) + 1; } } // Sort tags by count (most popular first) final sortedTags = tagCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); // Get recent recipes (last 6) final recent = allRecipes.take(6).toList(); if (mounted) { setState(() { _allRecipes = allRecipes; _favoriteRecipes = favorites; _recentRecipes = recent; _tagCounts = Map.fromEntries(sortedTags); _isLoading = false; }); } } catch (e) { Logger.error('Failed to load home data', e); if (mounted) { setState(() { _isLoading = false; }); } } } void _navigateToRecipe(RecipeModel recipe) async { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => AddRecipeScreen(recipe: recipe, viewMode: true), ), ); // Reload data if recipe was edited if (result == true) { await _loadData(); } } void _navigateToRecipes() { final scaffold = context.findAncestorStateOfType(); scaffold?.navigateToRecipes(); } void _navigateToFavourites() async { final scaffold = context.findAncestorStateOfType(); final hasChanges = await scaffold?.navigateToFavourites() ?? false; if (hasChanges) { await _loadData(); } } void _navigateToTag(String tag) { // Navigate to recipes screen with tag filter // For now, just navigate to recipes - could be enhanced to filter by tag _navigateToRecipes(); } @override Widget build(BuildContext context) { return Scaffold( appBar: PrimaryAppBar(title: 'Home'), body: _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _loadData, child: _allRecipes.isEmpty ? _buildEmptyState() : SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stats Section _buildStatsSection(), const SizedBox(height: 24), // Featured Recipes (Recent) if (_recentRecipes.isNotEmpty) ...[ _buildSectionHeader( 'Recent Recipes', onSeeAll: _navigateToRecipes, ), const SizedBox(height: 12), _buildFeaturedRecipes(), const SizedBox(height: 24), ], // Popular Tags if (_tagCounts.isNotEmpty) ...[ _buildSectionHeader('Popular Tags'), const SizedBox(height: 12), _buildTagsSection(), const SizedBox(height: 24), ], // Quick Favorites if (_favoriteRecipes.isNotEmpty) ...[ _buildSectionHeader( 'Favorites', onSeeAll: _navigateToFavourites, ), const SizedBox(height: 12), _buildFavoritesSection(), const SizedBox(height: 24), ], ], ), ), ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.restaurant_menu, size: 80, color: Colors.grey[400], ), const SizedBox(height: 24), Text( 'No recipes yet', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Colors.grey[600], ), ), const SizedBox(height: 8), Text( 'Start by adding your first recipe!', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey[500], ), ), const SizedBox(height: 32), ElevatedButton.icon( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const AddRecipeScreen(), ), ).then((_) => _loadData()); }, icon: const Icon(Icons.add), label: const Text('Add Recipe'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ), ); } Widget _buildStatsSection() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: _buildStatCard( icon: Icons.restaurant_menu, label: 'Total Recipes', value: '${_allRecipes.length}', color: Colors.blue, ), ), const SizedBox(width: 12), Expanded( child: _buildStatCard( icon: Icons.favorite, label: 'Favorites', value: '${_favoriteRecipes.length}', color: Colors.red, ), ), const SizedBox(width: 12), Expanded( child: _buildStatCard( icon: Icons.local_offer, label: 'Tags', value: '${_tagCounts.length}', color: Colors.green, ), ), ], ), ); } Widget _buildStatCard({ required IconData icon, required String label, required String value, required Color color, }) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 24), ), const SizedBox(height: 12), Text( value, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 4), Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey[600], ), ), ], ), ), ); } Widget _buildSectionHeader(String title, {VoidCallback? onSeeAll}) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), if (onSeeAll != null) TextButton( onPressed: onSeeAll, child: const Text('See All'), ), ], ), ); } Widget _buildFeaturedRecipes() { return SizedBox( height: 240, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _recentRecipes.length, itemBuilder: (context, index) { final recipe = _recentRecipes[index]; return _buildRecipeCard(recipe, isHorizontal: true); }, ), ); } Widget _buildRecipeCard(RecipeModel recipe, {bool isHorizontal = false}) { final hasMedia = recipe.imageUrls.isNotEmpty || recipe.videoUrls.isNotEmpty; final firstImage = recipe.imageUrls.isNotEmpty ? recipe.imageUrls.first : null; final firstVideo = recipe.imageUrls.isEmpty && recipe.videoUrls.isNotEmpty ? recipe.videoUrls.first : null; return GestureDetector( onTap: () => _navigateToRecipe(recipe), child: Container( width: isHorizontal ? 280 : double.infinity, margin: EdgeInsets.only( right: isHorizontal ? 12 : 0, bottom: isHorizontal ? 0 : 12, ), child: Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Image/Video thumbnail ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), child: Container( height: isHorizontal ? 130 : 180, width: double.infinity, color: Colors.grey[200], child: hasMedia ? (firstImage != null ? Image.network( firstImage, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildPlaceholder(), ) : firstVideo != null ? _VideoThumbnailPreview(videoUrl: firstVideo) : _buildPlaceholder()) : _buildPlaceholder(), ), ), // Content Padding( padding: const EdgeInsets.all(10), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( recipe.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, fontSize: 15, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), if (recipe.rating > 0) ...[ Icon( Icons.star, size: 16, color: Colors.amber, ), const SizedBox(width: 4), Text( '${recipe.rating}', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontSize: 12, fontWeight: FontWeight.w600, ), ), ], if (recipe.isFavourite) ...[ if (recipe.rating > 0) const SizedBox(width: 8), Icon( Icons.favorite, size: 16, color: Colors.red, ), ], ], ), ), ], ), ), ), ); } Widget _buildPlaceholder() { return Container( color: Colors.grey[200], child: const Center( child: Icon( Icons.restaurant_menu, size: 48, color: Colors.grey, ), ), ); } Widget _buildTagsSection() { final topTags = _tagCounts.entries.take(10).toList(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap( spacing: 8, runSpacing: 8, children: topTags.map((entry) { return ActionChip( avatar: CircleAvatar( backgroundColor: Colors.blue.withValues(alpha: 0.2), radius: 12, child: Text( '${entry.value}', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), ), ), label: Text(entry.key), onPressed: () => _navigateToTag(entry.key), backgroundColor: Colors.blue.withValues(alpha: 0.1), ); }).toList(), ), ); } Widget _buildFavoritesSection() { final favoritesToShow = _favoriteRecipes.take(3).toList(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: favoritesToShow.map((recipe) { return _buildFavoriteCard(recipe); }).toList(), ), ); } Widget _buildFavoriteCard(RecipeModel recipe) { final hasMedia = recipe.imageUrls.isNotEmpty || recipe.videoUrls.isNotEmpty; final firstImage = recipe.imageUrls.isNotEmpty ? recipe.imageUrls.first : null; final firstVideo = recipe.imageUrls.isEmpty && recipe.videoUrls.isNotEmpty ? recipe.videoUrls.first : null; return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: InkWell( onTap: () => _navigateToRecipe(recipe), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // Thumbnail ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( width: 80, height: 80, color: Colors.grey[200], child: hasMedia ? (firstImage != null ? Image.network( firstImage, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildPlaceholder(), ) : firstVideo != null ? _VideoThumbnailPreview(videoUrl: firstVideo) : _buildPlaceholder()) : _buildPlaceholder(), ), ), const SizedBox(width: 12), // Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recipe.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (recipe.description != null && recipe.description!.isNotEmpty) ...[ const SizedBox(height: 4), Text( recipe.description!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey[600], ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 8), Row( children: [ if (recipe.rating > 0) ...[ Icon(Icons.star, size: 16, color: Colors.amber), const SizedBox(width: 4), Text( '${recipe.rating}', style: Theme.of(context).textTheme.bodySmall, ), ], if (recipe.tags.isNotEmpty) ...[ if (recipe.rating > 0) const SizedBox(width: 12), Icon(Icons.local_offer, size: 14, color: Colors.grey[600]), const SizedBox(width: 4), Text( '${recipe.tags.length}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey[600], ), ), ], ], ), ], ), ), Icon( Icons.favorite, color: Colors.red, size: 20, ), ], ), ), ), ); } } /// Widget that displays a video thumbnail preview with a play button overlay. class _VideoThumbnailPreview extends StatefulWidget { final String videoUrl; const _VideoThumbnailPreview({ required this.videoUrl, }); @override State<_VideoThumbnailPreview> createState() => _VideoThumbnailPreviewState(); } class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> { VideoPlayerController? _controller; bool _isInitialized = false; bool _hasError = false; @override void initState() { super.initState(); _extractThumbnail(); } Future _extractThumbnail() async { try { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); await _controller!.initialize(); if (mounted && _controller != null) { await _controller!.seekTo(Duration.zero); _controller!.pause(); setState(() { _isInitialized = true; }); } } catch (e) { Logger.warning('Failed to extract video thumbnail: $e'); if (mounted) { setState(() { _hasError = true; }); } } } @override void dispose() { _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (_hasError || !_isInitialized || _controller == null) { return Stack( fit: StackFit.expand, children: [ Container( color: Colors.black87, child: const Center( child: Icon( Icons.play_circle_filled, size: 40, color: Colors.white70, ), ), ), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.4), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow, size: 32, color: Colors.white, ), ), ), ], ); } return Stack( fit: StackFit.expand, children: [ AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow, size: 32, color: Colors.white, ), ), ), ], ); } }