import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_colors.dart'; import '../../shared/models/recipe.dart'; import '../../shared/models/saved_recipe.dart'; import 'recipe_provider.dart'; /// Unified detail screen for both recommendation recipes and saved recipes. /// /// Pass a [Recipe] (from recommendations) or a [SavedRecipe] (from saved list) /// via GoRouter's `extra` parameter. class RecipeDetailScreen extends ConsumerStatefulWidget { final Recipe? recipe; final SavedRecipe? saved; const RecipeDetailScreen({super.key, this.recipe, this.saved}) : assert(recipe != null || saved != null, 'Provide either recipe or saved'); @override ConsumerState createState() => _RecipeDetailScreenState(); } class _RecipeDetailScreenState extends ConsumerState { bool _isSaving = false; // ── Unified accessors ──────────────────────────────────────────────────── String get _title => widget.recipe?.title ?? widget.saved!.title; String? get _description => widget.recipe?.description ?? widget.saved!.description; String? get _imageUrl => widget.recipe?.imageUrl.isNotEmpty == true ? widget.recipe!.imageUrl : widget.saved?.imageUrl; String? get _cuisine => widget.recipe?.cuisine ?? widget.saved!.cuisine; String? get _difficulty => widget.recipe?.difficulty ?? widget.saved!.difficulty; int? get _prepTimeMin => widget.recipe?.prepTimeMin ?? widget.saved!.prepTimeMin; int? get _cookTimeMin => widget.recipe?.cookTimeMin ?? widget.saved!.cookTimeMin; int? get _servings => widget.recipe?.servings ?? widget.saved!.servings; List get _ingredients => widget.recipe?.ingredients ?? widget.saved!.ingredients; List get _steps => widget.recipe?.steps ?? widget.saved!.steps; List get _tags => widget.recipe?.tags ?? widget.saved!.tags; NutritionInfo? get _nutrition => widget.recipe?.nutrition ?? widget.saved!.nutrition; bool get _isFromSaved => widget.saved != null; @override Widget build(BuildContext context) { final savedNotifier = ref.watch(savedRecipesProvider.notifier); final isSaved = _isFromSaved || (widget.recipe != null && savedNotifier.isSaved(_title)); return Scaffold( body: CustomScrollView( slivers: [ _buildAppBar(context), SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title Text( _title, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith(fontWeight: FontWeight.bold), ), if (_description != null && _description!.isNotEmpty) ...[ const SizedBox(height: 8), Text( _description!, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: AppColors.textSecondary), ), ], const SizedBox(height: 16), _MetaChips( prepTimeMin: _prepTimeMin, cookTimeMin: _cookTimeMin, difficulty: _difficulty, cuisine: _cuisine, servings: _servings, ), if (_nutrition != null) ...[ const SizedBox(height: 16), _NutritionCard(nutrition: _nutrition!), ], if (_tags.isNotEmpty) ...[ const SizedBox(height: 12), _TagsRow(tags: _tags), ], ], ), ), const Divider(height: 32), _IngredientsSection(ingredients: _ingredients), const Divider(height: 32), _StepsSection(steps: _steps), const SizedBox(height: 24), // Save / Unsave button Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _SaveButton( isSaved: isSaved, isLoading: _isSaving, onPressed: () => _toggleSave(context, isSaved), ), ), const SizedBox(height: 32), ], ), ), ], ), ); } Widget _buildAppBar(BuildContext context) { return SliverAppBar( expandedHeight: 280, pinned: true, flexibleSpace: FlexibleSpaceBar( background: _imageUrl != null && _imageUrl!.isNotEmpty ? CachedNetworkImage( imageUrl: _imageUrl!, fit: BoxFit.cover, placeholder: (_, __) => Container(color: Colors.grey[200]), errorWidget: (_, __, ___) => _PlaceholderImage(), ) : _PlaceholderImage(), ), ); } Future _toggleSave(BuildContext context, bool isSaved) async { if (_isSaving) return; HapticFeedback.lightImpact(); setState(() => _isSaving = true); final notifier = ref.read(savedRecipesProvider.notifier); try { if (isSaved) { final id = _isFromSaved ? widget.saved!.id : notifier.savedId(_title); if (id != null) { final ok = await notifier.delete(id); if (!ok && context.mounted) { _showSnack(context, 'Не удалось удалить из сохранённых'); } else if (ok && _isFromSaved && context.mounted) { Navigator.of(context).pop(); } } } else if (widget.recipe != null) { final saved = await notifier.save(widget.recipe!); if (saved == null && context.mounted) { _showSnack(context, 'Не удалось сохранить рецепт'); } } } finally { if (mounted) setState(() => _isSaving = false); } } void _showSnack(BuildContext context, String message) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } } // --------------------------------------------------------------------------- // Sub-widgets // --------------------------------------------------------------------------- class _PlaceholderImage extends StatelessWidget { @override Widget build(BuildContext context) => Container( color: AppColors.primary.withValues(alpha: 0.15), child: const Center(child: Icon(Icons.restaurant, size: 64)), ); } class _MetaChips extends StatelessWidget { final int? prepTimeMin; final int? cookTimeMin; final String? difficulty; final String? cuisine; final int? servings; const _MetaChips({ this.prepTimeMin, this.cookTimeMin, this.difficulty, this.cuisine, this.servings, }); @override Widget build(BuildContext context) { final totalMin = (prepTimeMin ?? 0) + (cookTimeMin ?? 0); return Wrap( spacing: 8, runSpacing: 4, children: [ if (totalMin > 0) _Chip(icon: Icons.access_time, label: '$totalMin мин'), if (difficulty != null) _Chip(icon: Icons.bar_chart, label: _difficultyLabel(difficulty!)), if (cuisine != null) _Chip(icon: Icons.public, label: _cuisineLabel(cuisine!)), if (servings != null) _Chip(icon: Icons.people, label: '$servings порц.'), ], ); } String _difficultyLabel(String d) => switch (d) { 'easy' => 'Легко', 'medium' => 'Средне', 'hard' => 'Сложно', _ => d, }; String _cuisineLabel(String c) => switch (c) { 'russian' => 'Русская', 'asian' => 'Азиатская', 'european' => 'Европейская', 'mediterranean' => 'Средиземноморская', 'american' => 'Американская', _ => 'Другая', }; } class _Chip extends StatelessWidget { final IconData icon; final String label; const _Chip({required this.icon, required this.label}); @override Widget build(BuildContext context) => Chip( avatar: Icon(icon, size: 14), label: Text(label, style: const TextStyle(fontSize: 12)), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ); } class _NutritionCard extends StatelessWidget { final NutritionInfo nutrition; const _NutritionCard({required this.nutrition}); @override Widget build(BuildContext context) { return Card( color: AppColors.primary.withValues(alpha: 0.3), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'КБЖУ на порцию', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(width: 4), Tooltip( message: 'Значения рассчитаны приблизительно с помощью ИИ', child: Text( '≈', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _NutCell( label: 'Калории', value: '${nutrition.calories.round()}'), _NutCell( label: 'Белки', value: '${nutrition.proteinG.round()} г'), _NutCell( label: 'Жиры', value: '${nutrition.fatG.round()} г'), _NutCell( label: 'Углев.', value: '${nutrition.carbsG.round()} г'), ], ), ], ), ), ); } } class _NutCell extends StatelessWidget { final String label; final String value; const _NutCell({required this.label, required this.value}); @override Widget build(BuildContext context) => Column( children: [ Text( value, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 15), ), const SizedBox(height: 2), Text( label, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary), ), ], ); } class _TagsRow extends StatelessWidget { final List tags; const _TagsRow({required this.tags}); @override Widget build(BuildContext context) { return Wrap( spacing: 6, runSpacing: 4, children: tags .map( (t) => Chip( label: Text(t, style: const TextStyle(fontSize: 11)), backgroundColor: AppColors.primary.withValues(alpha: 0.15), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ), ) .toList(), ); } } class _IngredientsSection extends StatelessWidget { final List ingredients; const _IngredientsSection({required this.ingredients}); @override Widget build(BuildContext context) { if (ingredients.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Ингредиенты', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), ...ingredients.map( (ing) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ const Icon(Icons.circle, size: 6, color: AppColors.primary), const SizedBox(width: 10), Expanded(child: Text(ing.name)), Text( '${_formatAmount(ing.amount)} ${ing.unit}', style: const TextStyle( color: AppColors.textSecondary, fontSize: 13), ), ], ), ), ), ], ), ); } String _formatAmount(double amount) { if (amount == amount.truncate()) return amount.toInt().toString(); return amount.toStringAsFixed(1); } } class _StepsSection extends StatelessWidget { final List steps; const _StepsSection({required this.steps}); @override Widget build(BuildContext context) { if (steps.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Приготовление', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), ...steps.map((step) => _StepTile(step: step)), ], ), ); } } class _StepTile extends StatelessWidget { final RecipeStep step; const _StepTile({required this.step}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Step number badge Container( width: 28, height: 28, decoration: const BoxDecoration( color: AppColors.primary, shape: BoxShape.circle, ), child: Center( child: Text( '${step.number}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(step.description), if (step.timerSeconds != null) ...[ const SizedBox(height: 4), Row( children: [ const Icon(Icons.timer_outlined, size: 14, color: AppColors.primary), const SizedBox(width: 4), Text( _formatTimer(step.timerSeconds!), style: const TextStyle( color: AppColors.primary, fontSize: 12), ), ], ), ], ], ), ), ], ), ); } String _formatTimer(int seconds) { if (seconds < 60) return '$seconds сек'; final m = seconds ~/ 60; final s = seconds % 60; return s == 0 ? '$m мин' : '$m мин $s сек'; } } class _SaveButton extends StatelessWidget { final bool isSaved; final bool isLoading; final VoidCallback onPressed; const _SaveButton({ required this.isSaved, required this.isLoading, required this.onPressed, }); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: isLoading ? null : onPressed, icon: isLoading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(isSaved ? Icons.favorite : Icons.favorite_border), label: Text(isSaved ? 'Сохранено' : 'Сохранить'), style: ElevatedButton.styleFrom( backgroundColor: isSaved ? Colors.red[100] : AppColors.primary, foregroundColor: isSaved ? Colors.red[800] : Colors.white, ), ), ); } }