Files
food-ai/client/lib/features/recipes/saved_recipes_screen.dart
dbastrikin a0ebd6cc0b feat: apply iOS-style theme and replace removed color constants
Switch AppColors to iOS system palette (007AFF blue, F2F2F7 grouped
background, separator, label hierarchy) and rewrite AppTheme with
iOS-inspired Material 3 tokens (no elevation, negative letter-spacing,
50px buttons, 12px radii). Replace removed primaryLight/accent references
in recipe screens with primary.withValues(alpha:0.15) and primary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:54:54 +02:00

272 lines
8.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/saved_recipe.dart';
import 'recipe_provider.dart';
class SavedRecipesScreen extends ConsumerWidget {
const SavedRecipesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(savedRecipesProvider);
return state.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 12),
const Text('Не удалось загрузить сохранённые рецепты'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => ref.read(savedRecipesProvider.notifier).load(),
child: const Text('Повторить'),
),
],
),
),
data: (recipes) => recipes.isEmpty
? const _EmptyState()
: _SavedList(recipes: recipes),
);
}
}
// ---------------------------------------------------------------------------
// List
// ---------------------------------------------------------------------------
class _SavedList extends StatelessWidget {
final List<SavedRecipe> recipes;
const _SavedList({required this.recipes});
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: recipes.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) =>
_SavedRecipeItem(recipe: recipes[index]),
);
}
}
// ---------------------------------------------------------------------------
// Single item with swipe-to-delete
// ---------------------------------------------------------------------------
class _SavedRecipeItem extends ConsumerWidget {
final SavedRecipe recipe;
const _SavedRecipeItem({required this.recipe});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Dismissible(
key: ValueKey(recipe.id),
direction: DismissDirection.endToStart,
background: _DeleteBackground(),
confirmDismiss: (_) => _confirmDelete(context),
onDismissed: (_) async {
final ok =
await ref.read(savedRecipesProvider.notifier).delete(recipe.id);
if (!ok && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Не удалось удалить рецепт')),
);
}
},
child: Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.push('/recipe-detail', extra: recipe),
child: Row(
children: [
// Thumbnail
_Thumbnail(imageUrl: recipe.imageUrl),
// Info
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
recipe.title,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (recipe.nutrition != null) ...[
const SizedBox(height: 4),
Text(
'${recipe.nutrition!.calories.round()} ккал · '
'${recipe.nutrition!.proteinG.round()} б · '
'${recipe.nutrition!.fatG.round()} ж · '
'${recipe.nutrition!.carbsG.round()} у',
style: const TextStyle(
fontSize: 11, color: AppColors.textSecondary),
),
],
if (recipe.prepTimeMin != null ||
recipe.cookTimeMin != null) ...[
const SizedBox(height: 4),
Text(
_timeLabel(recipe.prepTimeMin, recipe.cookTimeMin),
style: const TextStyle(
fontSize: 11, color: AppColors.textSecondary),
),
],
],
),
),
),
// Delete button
IconButton(
icon: const Icon(Icons.delete_outline,
color: AppColors.textSecondary),
onPressed: () async {
final confirmed = await _confirmDelete(context);
if (confirmed == true && context.mounted) {
final ok = await ref
.read(savedRecipesProvider.notifier)
.delete(recipe.id);
if (!ok && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось удалить рецепт')),
);
}
}
},
),
],
),
),
),
);
}
String _timeLabel(int? prep, int? cook) {
final total = (prep ?? 0) + (cook ?? 0);
return total > 0 ? '$total мин' : '';
}
Future<bool?> _confirmDelete(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Удалить рецепт?'),
content: Text('«${recipe.title}» будет удалён из сохранённых.'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Отмена'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Удалить'),
),
],
),
);
}
}
class _Thumbnail extends StatelessWidget {
final String? imageUrl;
const _Thumbnail({this.imageUrl});
@override
Widget build(BuildContext context) {
if (imageUrl == null || imageUrl!.isEmpty) {
return Container(
width: 80,
height: 80,
color: AppColors.primary.withValues(alpha: 0.15),
child: const Icon(Icons.restaurant),
);
}
return CachedNetworkImage(
imageUrl: imageUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholder: (_, __) =>
Container(width: 80, height: 80, color: Colors.grey[200]),
errorWidget: (_, __, ___) => Container(
width: 80,
height: 80,
color: AppColors.primary.withValues(alpha: 0.15),
child: const Icon(Icons.restaurant),
),
);
}
}
class _DeleteBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.error,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
);
}
}
// ---------------------------------------------------------------------------
// Empty state
// ---------------------------------------------------------------------------
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.favorite_border,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Нет сохранённых рецептов',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
Text(
'Сохраняйте рецепты из рекомендаций,\nнажимая на ♡',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[500]),
),
],
),
),
);
}
}