From b568d422156c95dd2e982a52616fab1ed94e6e12 Mon Sep 17 00:00:00 2001 From: gitea Date: Fri, 14 Nov 2025 01:03:19 +0100 Subject: [PATCH] home screen content addded --- lib/ui/home/home_screen.dart | 731 ++++++++++++++++-- .../navigation/main_navigation_scaffold.dart | 11 + 2 files changed, 673 insertions(+), 69 deletions(-) diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 4e7c840..cc6b816 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; -import '../../data/local/models/item.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 local storage and cached content. +/// Home screen showing recipes overview, stats, tags, and favorites. class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -12,37 +17,108 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - List _items = []; + RecipeService? _recipeService; + List _allRecipes = []; + List _favoriteRecipes = []; + List _recentRecipes = []; + Map _tagCounts = {}; bool _isLoading = true; @override void initState() { super.initState(); - _loadItems(); + _initializeService(); } - Future _loadItems() async { + Future _initializeService() async { try { - final localStorageService = ServiceLocator.instance.localStorageService; - if (localStorageService == null) { + _recipeService = ServiceLocator.instance.recipeService; + if (_recipeService != null) { + await _loadData(); + } + } catch (e) { + Logger.error('Failed to initialize home screen', e); + if (mounted) { setState(() { _isLoading = false; }); - return; } + } + } + + 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(); - final items = await localStorageService.getAllItems(); - setState(() { - _items = items; - _isLoading = false; - }); + if (mounted) { + setState(() { + _allRecipes = allRecipes; + _favoriteRecipes = favorites; + _recentRecipes = recent; + _tagCounts = Map.fromEntries(sortedTags); + _isLoading = false; + }); + } } catch (e) { - setState(() { - _isLoading = false; - }); + 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( @@ -50,68 +126,585 @@ class _HomeScreenState extends State { body: _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( - onRefresh: _loadItems, - child: _items.isEmpty - ? const Center( + onRefresh: _loadData, + child: _allRecipes.isEmpty + ? _buildEmptyState() + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.storage_outlined, - size: 64, - color: Colors.grey, - ), - SizedBox(height: 16), - Text( - 'No items in local storage', - style: TextStyle( - fontSize: 16, - color: Colors.grey, + // 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), + ], ], ), - ) - : ListView.builder( - itemCount: _items.length, - itemBuilder: (context, index) { - final item = _items[index]; - return ListTile( - leading: const Icon(Icons.data_object), - title: Text(item.id), - subtitle: Text( - 'Created: ${DateTime.fromMillisecondsSinceEpoch(item.createdAt).toString().split('.')[0]}', + ), + ), + ); + } + + 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, ), - trailing: IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () async { - final localStorageService = ServiceLocator.instance.localStorageService; - await localStorageService?.deleteItem(item.id); - _loadItems(); - }, + ], + 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], + ), ), - ); - }, + ], + ], ), - ), - floatingActionButton: ServiceLocator.instance.localStorageService != null - ? FloatingActionButton( - onPressed: () async { - final localStorageService = ServiceLocator.instance.localStorageService; - final item = Item( - id: 'item-${DateTime.now().millisecondsSinceEpoch}', - data: { - 'name': 'New Item', - 'timestamp': DateTime.now().toIso8601String(), - }, - ); - await localStorageService!.insertItem(item); - _loadItems(); - }, - child: const Icon(Icons.add), - ) - : null, + ], + ), + ), + 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, + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/navigation/main_navigation_scaffold.dart b/lib/ui/navigation/main_navigation_scaffold.dart index f557e42..d4229aa 100644 --- a/lib/ui/navigation/main_navigation_scaffold.dart +++ b/lib/ui/navigation/main_navigation_scaffold.dart @@ -97,6 +97,17 @@ class MainNavigationScaffoldState extends State { }); } + /// Public method to navigate to Recipes tab. + void navigateToRecipes() { + if (_isLoggedIn) { + setState(() { + _currentIndex = 1; // Recipes tab + }); + } else { + navigateToUser(); + } + } + /// Public method to navigate to Favourites screen (used from Recipes AppBar). /// Returns true if any changes were made that require refreshing the Recipes list. Future navigateToFavourites() async {