2e117718a7
- Added localization support for Swedish and English languages. - Integrated localized strings for user messages in the API error mapper. - Updated UI components to use localized strings for labels and messages. - Ensured all error messages are context-aware and utilize the localization framework. - Created regression test to prevent common ASCII fallbacks in Swedish UI text.
191 lines
6.4 KiB
Dart
191 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../core/api/api_error_mapper.dart';
|
|
import '../../../core/api/api_exception.dart';
|
|
import '../../../core/ui/async_state_views.dart';
|
|
import '../../auth/data/auth_providers.dart';
|
|
import '../data/recipe_providers.dart';
|
|
import '../domain/recipe.dart';
|
|
|
|
class RecipeDetailScreen extends ConsumerWidget {
|
|
final int recipeId;
|
|
|
|
const RecipeDetailScreen({super.key, required this.recipeId});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final recipeAsync = ref.watch(recipeDetailProvider(recipeId));
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(recipeAsync.valueOrNull?.title ?? 'Recept'),
|
|
actions: recipeAsync.valueOrNull == null
|
|
? []
|
|
: [
|
|
IconButton(
|
|
tooltip: 'Redigera',
|
|
icon: const Icon(Icons.edit_outlined),
|
|
onPressed: () =>
|
|
context.push('/recipes/$recipeId/edit'),
|
|
),
|
|
_DeleteButton(recipe: recipeAsync.value!),
|
|
],
|
|
),
|
|
body: recipeAsync.when(
|
|
loading: () => const LoadingStateView(label: 'Laddar recept...'),
|
|
error: (error, _) => ErrorStateView(
|
|
message: mapErrorToUserMessage(error, context),
|
|
onRetry: () => ref.invalidate(recipeDetailProvider(recipeId)),
|
|
),
|
|
data: (recipe) => _RecipeBody(recipe: recipe),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeleteButton extends ConsumerWidget {
|
|
final Recipe recipe;
|
|
|
|
const _DeleteButton({required this.recipe});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return IconButton(
|
|
tooltip: 'Ta bort',
|
|
icon: const Icon(Icons.delete_outline),
|
|
onPressed: () => _confirmDelete(context, ref),
|
|
);
|
|
}
|
|
|
|
Future<void> _confirmDelete(BuildContext context, WidgetRef ref) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => AlertDialog(
|
|
title: const Text('Ta bort recept?'),
|
|
content: Text(
|
|
'Vill du ta bort "${recipe.title}"? Åtgärden kan inte ångras.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Avbryt'),
|
|
),
|
|
FilledButton(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.error),
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Ta bort'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed != true || !context.mounted) return;
|
|
|
|
try {
|
|
final token = await ref.read(authStateProvider.future);
|
|
await ref.read(recipeRepositoryProvider).deleteRecipe(recipe.id,
|
|
token: token);
|
|
ref.invalidate(recipesProvider);
|
|
if (context.mounted) context.go('/recipes');
|
|
} on ApiException catch (e) {
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _RecipeBody extends StatelessWidget {
|
|
final Recipe recipe;
|
|
|
|
const _RecipeBody({required this.recipe});
|
|
|
|
String _formatQty(double qty) {
|
|
if (qty == 0) return '';
|
|
return qty == qty.truncateToDouble()
|
|
? qty.toInt().toString()
|
|
: qty.toString();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (recipe.imageUrl != null)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: AspectRatio(
|
|
aspectRatio: 16 / 9,
|
|
child: Image.network(
|
|
recipe.imageUrl!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
|
),
|
|
),
|
|
),
|
|
if (recipe.imageUrl != null) const SizedBox(height: 20),
|
|
Text(recipe.title, style: theme.textTheme.headlineSmall),
|
|
if (recipe.description != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(recipe.description!,
|
|
style: theme.textTheme.bodyMedium
|
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
|
|
],
|
|
if (recipe.servings != null) ...[
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.people_outline, size: 16),
|
|
const SizedBox(width: 4),
|
|
Text('${recipe.servings} portioner',
|
|
style: theme.textTheme.bodySmall),
|
|
],
|
|
),
|
|
],
|
|
if (recipe.ingredients.isNotEmpty) ...[
|
|
const SizedBox(height: 24),
|
|
Text('Ingredienser', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
...recipe.ingredients.map((ing) {
|
|
final qtyStr = _formatQty(ing.quantity);
|
|
final parts = [
|
|
if (qtyStr.isNotEmpty) qtyStr,
|
|
if (ing.unit.isNotEmpty) ing.unit,
|
|
ing.productName,
|
|
if (ing.note != null) '(${ing.note})',
|
|
];
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('• '),
|
|
Expanded(child: Text(parts.join(' '))),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
if (recipe.instructions != null &&
|
|
recipe.instructions!.isNotEmpty) ...[
|
|
const SizedBox(height: 24),
|
|
Text('Tillvägagångssätt', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
Text(recipe.instructions!,
|
|
style: theme.textTheme.bodyMedium
|
|
?.copyWith(height: 1.6)),
|
|
],
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|