home screen content addded

master
gitea 2 months ago
parent 5a2d287d00
commit b568d42215

@ -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<HomeScreen> {
List<Item> _items = [];
RecipeService? _recipeService;
List<RecipeModel> _allRecipes = [];
List<RecipeModel> _favoriteRecipes = [];
List<RecipeModel> _recentRecipes = [];
Map<String, int> _tagCounts = {};
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadItems();
_initializeService();
}
Future<void> _loadItems() async {
Future<void> _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<void> _loadData() async {
if (_recipeService == null) return;
try {
final allRecipes = await _recipeService!.getAllRecipes();
final favorites = await _recipeService!.getFavouriteRecipes();
// Calculate tag counts
final tagCounts = <String, int>{};
for (final recipe in allRecipes) {
for (final tag in recipe.tags) {
tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
}
}
final items = await localStorageService.getAllItems();
setState(() {
_items = items;
_isLoading = false;
});
// 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) {
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<MainNavigationScaffoldState>();
scaffold?.navigateToRecipes();
}
void _navigateToFavourites() async {
final scaffold = context.findAncestorStateOfType<MainNavigationScaffoldState>();
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<HomeScreen> {
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<void> _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,
),
),
),
],
);
}
}

@ -97,6 +97,17 @@ class MainNavigationScaffoldState extends State<MainNavigationScaffold> {
});
}
/// 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<bool> navigateToFavourites() async {

Loading…
Cancel
Save

Powered by TurnKey Linux.