import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_colors.dart'; import '../../shared/models/diary_entry.dart'; import '../../shared/models/home_summary.dart'; import '../../shared/models/meal_type.dart'; import '../menu/menu_provider.dart'; import '../profile/profile_provider.dart'; import 'home_provider.dart'; // ── Root screen ─────────────────────────────────────────────── class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final homeSummaryState = ref.watch(homeProvider); final profile = ref.watch(profileProvider).valueOrNull; final userName = profile?.name; final goalType = profile?.goal; final dailyGoal = profile?.dailyCalories ?? 0; final userMealTypes = profile?.mealTypes ?? const ['breakfast', 'lunch', 'dinner']; final selectedDate = ref.watch(selectedDateProvider); final dateString = formatDateForDiary(selectedDate); final diaryState = ref.watch(diaryProvider(dateString)); final entries = diaryState.valueOrNull ?? []; final loggedCalories = entries.fold( 0.0, (sum, entry) => sum + (entry.calories ?? 0)); final loggedProtein = entries.fold( 0.0, (sum, entry) => sum + (entry.proteinG ?? 0)); final loggedFat = entries.fold( 0.0, (sum, entry) => sum + (entry.fatG ?? 0)); final loggedCarbs = entries.fold( 0.0, (sum, entry) => sum + (entry.carbsG ?? 0)); final expiringSoon = homeSummaryState.valueOrNull?.expiringSoon ?? []; final recommendations = homeSummaryState.valueOrNull?.recommendations ?? []; return Scaffold( body: RefreshIndicator( onRefresh: () async { ref.read(homeProvider.notifier).load(); ref.invalidate(diaryProvider(dateString)); }, child: CustomScrollView( slivers: [ _AppBar(userName: userName), SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), sliver: SliverList( delegate: SliverChildListDelegate([ const SizedBox(height: 12), _DateSelector( selectedDate: selectedDate, onDateSelected: (date) => ref.read(selectedDateProvider.notifier).state = date, ), const SizedBox(height: 16), _CaloriesCard( loggedCalories: loggedCalories, dailyGoal: dailyGoal, goalType: goalType, ), const SizedBox(height: 12), _MacrosRow( proteinG: loggedProtein, fatG: loggedFat, carbsG: loggedCarbs, ), const SizedBox(height: 16), _DailyMealsSection( mealTypeIds: userMealTypes, entries: entries, dateString: dateString, ), if (expiringSoon.isNotEmpty) ...[ const SizedBox(height: 16), _ExpiringBanner(items: expiringSoon), ], const SizedBox(height: 16), _QuickActionsRow(date: dateString), if (recommendations.isNotEmpty) ...[ const SizedBox(height: 20), _SectionTitle('Рекомендуем приготовить'), const SizedBox(height: 12), _RecommendationsRow(recipes: recommendations), ], ]), ), ), ], ), ), ); } } // ── App bar ─────────────────────────────────────────────────── class _AppBar extends StatelessWidget { final String? userName; const _AppBar({this.userName}); String get _greetingBase { final hour = DateTime.now().hour; if (hour < 12) return 'Доброе утро'; if (hour < 18) return 'Добрый день'; return 'Добрый вечер'; } String get _greeting { final name = userName; if (name != null && name.isNotEmpty) return '$_greetingBase, $name!'; return _greetingBase; } @override Widget build(BuildContext context) { final theme = Theme.of(context); return SliverAppBar( pinned: false, floating: true, title: Text(_greeting, style: theme.textTheme.titleMedium), ); } } // ── Date selector ───────────────────────────────────────────── class _DateSelector extends StatefulWidget { final DateTime selectedDate; final ValueChanged onDateSelected; const _DateSelector({ required this.selectedDate, required this.onDateSelected, }); @override State<_DateSelector> createState() => _DateSelectorState(); } class _DateSelectorState extends State<_DateSelector> { static const _weekDayShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; static const _monthNames = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря', ]; // Total days available in the past (index 0 = today, index N-1 = oldest) static const _totalDays = 365; static const _pillWidth = 48.0; static const _pillSpacing = 6.0; late final ScrollController _scrollController; String _formatSelectedDate(DateTime date) { final now = DateTime.now(); final dayName = _weekDayShort[date.weekday - 1]; final month = _monthNames[date.month - 1]; final yearSuffix = date.year != now.year ? ' ${date.year}' : ''; return '$dayName, ${date.day} $month$yearSuffix'; } // Index in the reversed list: 0 = today, 1 = yesterday, … int _indexForDate(DateTime date) { final today = DateTime.now(); final todayNormalized = DateTime(today.year, today.month, today.day); final dateNormalized = DateTime(date.year, date.month, date.day); return todayNormalized.difference(dateNormalized).inDays.clamp(0, _totalDays - 1); } double _offsetForIndex(int index) => index * (_pillWidth + _pillSpacing); void _selectPreviousDay() { final previousDay = widget.selectedDate.subtract(const Duration(days: 1)); final previousDayNormalized = DateTime(previousDay.year, previousDay.month, previousDay.day); final today = DateTime.now(); final oldestAllowed = DateTime(today.year, today.month, today.day) .subtract(const Duration(days: _totalDays - 1)); if (!previousDayNormalized.isBefore(oldestAllowed)) { widget.onDateSelected(previousDayNormalized); } } void _selectNextDay() { final today = DateTime.now(); final todayNormalized = DateTime(today.year, today.month, today.day); final nextDay = widget.selectedDate.add(const Duration(days: 1)); final nextDayNormalized = DateTime(nextDay.year, nextDay.month, nextDay.day); if (!nextDayNormalized.isAfter(todayNormalized)) { widget.onDateSelected(nextDayNormalized); } } void _jumpToToday() { final today = DateTime.now(); widget.onDateSelected(DateTime(today.year, today.month, today.day)); if (_scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } @override void initState() { super.initState(); _scrollController = ScrollController(); } @override void didUpdateWidget(_DateSelector oldWidget) { super.didUpdateWidget(oldWidget); final oldIndex = _indexForDate(oldWidget.selectedDate); final newIndex = _indexForDate(widget.selectedDate); if (oldIndex != newIndex && _scrollController.hasClients) { _scrollController.animateTo( _offsetForIndex(newIndex), duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final today = DateTime.now(); final todayNormalized = DateTime(today.year, today.month, today.day); final selectedNormalized = DateTime( widget.selectedDate.year, widget.selectedDate.month, widget.selectedDate.day); final isToday = selectedNormalized == todayNormalized; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header row ────────────────────────────────────────── SizedBox( height: 36, child: Row( children: [ IconButton( icon: const Icon(Icons.chevron_left), iconSize: 20, visualDensity: VisualDensity.compact, onPressed: _selectPreviousDay, ), Expanded( child: Text( _formatSelectedDate(widget.selectedDate), style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), ), if (!isToday) TextButton( onPressed: _jumpToToday, style: TextButton.styleFrom( visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 8), ), child: Text( 'Сегодня', style: theme.textTheme.labelMedium ?.copyWith(color: theme.colorScheme.primary), ), ) else IconButton( icon: const Icon(Icons.chevron_right), iconSize: 20, visualDensity: VisualDensity.compact, onPressed: null, ), ], ), ), const SizedBox(height: 8), // ── Day strip ──────────────────────────────────────────── SizedBox( height: 56, // reverse: true → index 0 (today) sits at the right edge child: ListView.separated( controller: _scrollController, scrollDirection: Axis.horizontal, reverse: true, itemCount: _totalDays, separatorBuilder: (_, __) => const SizedBox(width: _pillSpacing), itemBuilder: (listContext, index) { final date = todayNormalized.subtract(Duration(days: index)); final isSelected = date == selectedNormalized; final isDayToday = date == todayNormalized; return GestureDetector( onTap: () => widget.onDateSelected(date), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: _pillWidth, decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _weekDayShort[date.weekday - 1], style: theme.textTheme.labelSmall?.copyWith( color: isSelected ? theme.colorScheme.onPrimary : theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( '${date.day}', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: isSelected || isDayToday ? FontWeight.w700 : FontWeight.normal, color: isSelected ? theme.colorScheme.onPrimary : isDayToday ? theme.colorScheme.primary : null, ), ), ], ), ), ); }, ), ), ], ); } } // ── Calories card ───────────────────────────────────────────── class _CaloriesCard extends StatelessWidget { final double loggedCalories; final int dailyGoal; final String? goalType; const _CaloriesCard({ required this.loggedCalories, required this.dailyGoal, required this.goalType, }); @override Widget build(BuildContext context) { if (dailyGoal == 0) return const SizedBox.shrink(); final theme = Theme.of(context); final logged = loggedCalories.toInt(); final rawProgress = dailyGoal > 0 ? loggedCalories / dailyGoal : 0.0; final isOverGoal = rawProgress > 1.0; final ringColor = _ringColorFor(rawProgress, goalType); final String secondaryLabel; final Color secondaryColor; if (isOverGoal) { final overBy = (loggedCalories - dailyGoal).toInt(); secondaryLabel = '+$overBy перебор'; secondaryColor = AppColors.error; } else { final remaining = (dailyGoal - loggedCalories).toInt(); secondaryLabel = 'осталось $remaining'; secondaryColor = AppColors.textSecondary; } return Card( child: Padding( padding: const EdgeInsets.all(20), child: Row( children: [ SizedBox( width: 160, height: 160, child: Stack( alignment: Alignment.center, children: [ CustomPaint( size: const Size(160, 160), painter: _CalorieRingPainter( rawProgress: rawProgress, ringColor: ringColor, ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$logged', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w700, color: ringColor, ), ), Text( 'ккал', style: theme.textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), const SizedBox(height: 4), Text( 'цель: $dailyGoal', style: theme.textTheme.labelSmall?.copyWith( color: AppColors.textSecondary, ), ), ], ), ], ), ), const SizedBox(width: 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _CalorieStat( label: 'Потреблено', value: '$logged ккал', valueColor: ringColor, ), const SizedBox(height: 12), _CalorieStat( label: isOverGoal ? 'Превышение' : 'Осталось', value: secondaryLabel, valueColor: secondaryColor, ), const SizedBox(height: 12), _CalorieStat( label: 'Цель', value: '$dailyGoal ккал', valueColor: AppColors.textPrimary, ), ], ), ), ], ), ), ); } } class _CalorieStat extends StatelessWidget { final String label; final String value; final Color valueColor; const _CalorieStat({ required this.label, required this.value, required this.valueColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: theme.textTheme.labelSmall?.copyWith(color: AppColors.textSecondary), ), const SizedBox(height: 2), Text( value, style: theme.textTheme.bodyMedium?.copyWith( color: valueColor, fontWeight: FontWeight.w600, ), ), ], ); } } // ── Macros row ──────────────────────────────────────────────── class _MacrosRow extends StatelessWidget { final double proteinG; final double fatG; final double carbsG; const _MacrosRow({ required this.proteinG, required this.fatG, required this.carbsG, }); @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: _MacroChip( label: 'Белки', value: '${proteinG.toStringAsFixed(1)} г', color: Colors.blue, ), ), const SizedBox(width: 8), Expanded( child: _MacroChip( label: 'Жиры', value: '${fatG.toStringAsFixed(1)} г', color: Colors.orange, ), ), const SizedBox(width: 8), Expanded( child: _MacroChip( label: 'Углеводы', value: '${carbsG.toStringAsFixed(1)} г', color: Colors.green, ), ), ], ); } } class _MacroChip extends StatelessWidget { final String label; final String value; final Color color; const _MacroChip({ required this.label, required this.value, required this.color, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Text( value, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w700, color: color, ), ), const SizedBox(height: 2), Text( label, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ); } } // ── Ring colour logic ────────────────────────────────────────── Color _ringColorFor(double rawProgress, String? goalType) { switch (goalType) { case 'lose': if (rawProgress >= 1.10) return AppColors.error; if (rawProgress >= 0.95) return AppColors.warning; if (rawProgress >= 0.75) return AppColors.success; return AppColors.primary; case 'maintain': if (rawProgress >= 1.25) return AppColors.error; if (rawProgress >= 1.11) return AppColors.warning; if (rawProgress >= 0.90) return AppColors.success; if (rawProgress >= 0.70) return AppColors.warning; return AppColors.primary; case 'gain': if (rawProgress < 0.60) return AppColors.error; if (rawProgress < 0.85) return AppColors.warning; if (rawProgress <= 1.15) return AppColors.success; return AppColors.primary; default: return AppColors.primary; } } // ── Ring painter ─────────────────────────────────────────────── class _CalorieRingPainter extends CustomPainter { final double rawProgress; final Color ringColor; const _CalorieRingPainter({ required this.rawProgress, required this.ringColor, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = (size.width - 20) / 2; const strokeWidth = 10.0; const overflowStrokeWidth = 6.0; const startAngle = -math.pi / 2; final trackPaint = Paint() ..color = AppColors.separator.withValues(alpha: 0.5) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; canvas.drawCircle(center, radius, trackPaint); final clampedProgress = rawProgress.clamp(0.0, 1.0); if (clampedProgress > 0) { final arcPaint = Paint() ..color = ringColor ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, clampedProgress * 2 * math.pi, false, arcPaint, ); } if (rawProgress > 1.0) { final overflowProgress = rawProgress - 1.0; final overflowPaint = Paint() ..color = ringColor.withValues(alpha: 0.70) ..style = PaintingStyle.stroke ..strokeWidth = overflowStrokeWidth ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, overflowProgress.clamp(0.0, 1.0) * 2 * math.pi, false, overflowPaint, ); } } @override bool shouldRepaint(_CalorieRingPainter oldDelegate) => oldDelegate.rawProgress != rawProgress || oldDelegate.ringColor != ringColor; } // ── Daily meals section ─────────────────────────────────────── class _DailyMealsSection extends ConsumerWidget { final List mealTypeIds; final List entries; final String dateString; const _DailyMealsSection({ required this.mealTypeIds, required this.entries, required this.dateString, }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Приёмы пищи', style: theme.textTheme.titleSmall), const SizedBox(height: 8), ...mealTypeIds.map((mealTypeId) { final mealTypeOption = mealTypeById(mealTypeId); if (mealTypeOption == null) return const SizedBox.shrink(); final mealEntries = entries .where((entry) => entry.mealType == mealTypeId) .toList(); return Padding( padding: const EdgeInsets.only(bottom: 8), child: _MealCard( mealTypeOption: mealTypeOption, entries: mealEntries, dateString: dateString, ), ); }), ], ); } } class _MealCard extends ConsumerWidget { final MealTypeOption mealTypeOption; final List entries; final String dateString; const _MealCard({ required this.mealTypeOption, required this.entries, required this.dateString, }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final totalCalories = entries.fold( 0.0, (sum, entry) => sum + (entry.calories ?? 0)); return Card( child: Column( children: [ // Header row Padding( padding: const EdgeInsets.fromLTRB(16, 12, 8, 8), child: Row( children: [ Text(mealTypeOption.emoji, style: const TextStyle(fontSize: 20)), const SizedBox(width: 8), Text(mealTypeOption.label, style: theme.textTheme.titleSmall), const Spacer(), if (totalCalories > 0) Text( '${totalCalories.toInt()} ккал', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), IconButton( icon: const Icon(Icons.add, size: 20), visualDensity: VisualDensity.compact, tooltip: 'Добавить блюдо', onPressed: () => context.push('/scan', extra: mealTypeOption.id), ), ], ), ), // Diary entries if (entries.isNotEmpty) ...[ const Divider(height: 1, indent: 16), ...entries.map((entry) => _DiaryEntryTile( entry: entry, onDelete: () => ref .read(diaryProvider(dateString).notifier) .remove(entry.id), )), ], ], ), ); } } class _DiaryEntryTile extends StatelessWidget { final DiaryEntry entry; final VoidCallback onDelete; const _DiaryEntryTile({required this.entry, required this.onDelete}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final calories = entry.calories?.toInt(); final hasProtein = (entry.proteinG ?? 0) > 0; final hasFat = (entry.fatG ?? 0) > 0; final hasCarbs = (entry.carbsG ?? 0) > 0; return ListTile( dense: true, title: Text(entry.name, style: theme.textTheme.bodyMedium), subtitle: (hasProtein || hasFat || hasCarbs) ? Text( [ if (hasProtein) 'Б ${entry.proteinG!.toStringAsFixed(1)}', if (hasFat) 'Ж ${entry.fatG!.toStringAsFixed(1)}', if (hasCarbs) 'У ${entry.carbsG!.toStringAsFixed(1)}', ].join(' '), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ) : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (calories != null) Text( '$calories ккал', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), IconButton( icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.error), visualDensity: VisualDensity.compact, onPressed: onDelete, ), ], ), ); } } // ── Expiring banner ─────────────────────────────────────────── class _ExpiringBanner extends StatelessWidget { final List items; const _ExpiringBanner({required this.items}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final color = theme.colorScheme.errorContainer; final onColor = theme.colorScheme.onErrorContainer; return GestureDetector( onTap: () => context.push('/products'), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.warning_amber_rounded, color: onColor, size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Истекает срок годности', style: theme.textTheme.labelMedium?.copyWith( color: onColor, fontWeight: FontWeight.w600), ), Text( items .take(3) .map((expiringSoonItem) => '${expiringSoonItem.name} — ${expiringSoonItem.expiryLabel}') .join(', '), style: theme.textTheme.bodySmall?.copyWith(color: onColor), ), ], ), ), Icon(Icons.chevron_right, color: onColor), ], ), ), ); } } // ── Quick actions ───────────────────────────────────────────── class _QuickActionsRow extends StatelessWidget { final String date; const _QuickActionsRow({required this.date}); @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: _ActionButton( icon: Icons.document_scanner_outlined, label: 'Сканировать', onTap: () => context.push('/scan'), ), ), const SizedBox(width: 8), Expanded( child: _ActionButton( icon: Icons.calendar_month_outlined, label: 'Меню', onTap: () => context.push('/menu'), ), ), const SizedBox(width: 8), Expanded( child: _ActionButton( icon: Icons.book_outlined, label: 'Дневник', onTap: () => context.push('/menu/diary', extra: date), ), ), ], ); } } class _ActionButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; const _ActionButton( {required this.icon, required this.label, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Column( children: [ Icon(icon, size: 24), const SizedBox(height: 6), Text(label, style: theme.textTheme.labelSmall, textAlign: TextAlign.center), ], ), ), ), ); } } // ── Section title ───────────────────────────────────────────── class _SectionTitle extends StatelessWidget { final String text; const _SectionTitle(this.text); @override Widget build(BuildContext context) => Text(text, style: Theme.of(context).textTheme.titleSmall); } // ── Recommendations row ─────────────────────────────────────── class _RecommendationsRow extends StatelessWidget { final List recipes; const _RecommendationsRow({required this.recipes}); @override Widget build(BuildContext context) { return SizedBox( height: 168, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: recipes.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) => _RecipeCard(recipe: recipes[index]), ), ); } } class _RecipeCard extends StatelessWidget { final HomeRecipe recipe; const _RecipeCard({required this.recipe}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return SizedBox( width: 140, child: Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: () {}, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ recipe.imageUrl.isNotEmpty ? CachedNetworkImage( imageUrl: recipe.imageUrl, height: 96, width: double.infinity, fit: BoxFit.cover, errorWidget: (_, __, ___) => _imagePlaceholder(), ) : _imagePlaceholder(), Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 4), child: Text( recipe.title, style: theme.textTheme.bodySmall ?.copyWith(fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), if (recipe.calories != null) Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), child: Text( '≈${recipe.calories!.toInt()} ккал', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), ), ), ], ), ), ), ); } Widget _imagePlaceholder() => Container( height: 96, color: Colors.grey.shade200, child: const Icon(Icons.restaurant, color: Colors.grey), ); }