Files
food-ai/client/lib/features/scan/recognition_history_screen.dart
dbastrikin 54b10d51e2 feat: Flutter client localisation (12 languages)
Add flutter_localizations + intl, 12 ARB files (en/ru/es/de/fr/it/pt/zh/ja/ko/ar/hi),
replace all hardcoded Russian UI strings with AppLocalizations, detect system locale
on first launch, localise bottom nav bar labels, document rule in CLAUDE.md.

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

133 lines
4.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:food_ai/l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../home/home_provider.dart';
import '../scan/dish_result_screen.dart';
import 'recognition_service.dart';
/// Full-screen page showing all dish recognition jobs.
class RecognitionHistoryScreen extends ConsumerWidget {
const RecognitionHistoryScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final jobsState = ref.watch(allJobsProvider);
return Scaffold(
appBar: AppBar(
title: Text(l10n.historyTitle),
),
body: jobsState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (recognitionError, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.historyLoadError),
const SizedBox(height: 12),
FilledButton(
onPressed: () => ref.invalidate(allJobsProvider),
child: Text(l10n.retry),
),
],
),
),
data: (jobs) {
if (jobs.isEmpty) {
return Center(child: Text(l10n.noHistory));
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: jobs.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) =>
_HistoryJobTile(job: jobs[index]),
);
},
),
);
}
}
class _HistoryJobTile extends ConsumerWidget {
final DishJobSummary job;
const _HistoryJobTile({required this.job});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final isDone = job.status == 'done';
final isFailed = job.status == 'failed';
final isProcessing =
job.status == 'processing' || job.status == 'pending';
final IconData statusIcon;
final Color statusColor;
if (isDone) {
statusIcon = Icons.check_circle_outline;
statusColor = Colors.green;
} else if (isFailed) {
statusIcon = Icons.error_outline;
statusColor = theme.colorScheme.error;
} else {
statusIcon = Icons.hourglass_top_outlined;
statusColor = theme.colorScheme.primary;
}
final dishName = job.result?.candidates.isNotEmpty == true
? job.result!.best.dishName
: null;
final String titleText;
if (isDone) {
titleText = dishName ?? l10n.dishRecognized;
} else if (isProcessing) {
titleText = l10n.recognizing;
} else {
titleText = l10n.recognitionError;
}
final contextParts = [
if (job.targetMealType != null) job.targetMealType!,
if (job.targetDate != null) job.targetDate!,
];
return Card(
child: ListTile(
leading: Icon(statusIcon, color: statusColor),
title: Text(titleText, style: theme.textTheme.bodyMedium),
subtitle: contextParts.isNotEmpty
? Text(
contextParts.join(' · '),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
)
: null,
onTap: isDone && job.result != null
? () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (sheetContext) => DishResultSheet(
dish: job.result!,
preselectedMealType: job.targetMealType,
jobId: job.id,
targetDate: job.targetDate,
createdAt: job.createdAt,
onAdded: () => Navigator.pop(sheetContext),
),
);
}
: null,
),
);
}
}