feat: show dish recognition result as bottom sheet on home screen

Remove "Определить блюдо" from ScanScreen and the /scan/dish route.
The + button on each meal card now triggers dish recognition inline —
picks image, shows loading dialog, then presents DishResultSheet as a
modal bottom sheet. After adding to diary the sheet closes and the user
stays on home.

Also fix Navigator.pop crash: showDialog uses the root navigator by
default, so capture Navigator.of(context, rootNavigator: true) before
the async gap and use it to close the loading dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-17 16:37:00 +02:00
parent 227780e1a9
commit a32d2960c4
5 changed files with 212 additions and 136 deletions

View File

@@ -3,17 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import '../../core/auth/auth_provider.dart';
import 'recognition_service.dart';
// Provider wired to the shared ApiClient.
final _recognitionServiceProvider = Provider<RecognitionService>((ref) {
return RecognitionService(ref.read(apiClientProvider));
});
/// Entry screen — lets the user choose how to add products.
/// If [GoRouterState.extra] is a non-null String, it is treated as a meal type ID
/// and the screen immediately opens the camera for dish recognition.
class ScanScreen extends ConsumerStatefulWidget {
const ScanScreen({super.key});
@@ -22,24 +14,6 @@ class ScanScreen extends ConsumerStatefulWidget {
}
class _ScanScreenState extends ConsumerState<ScanScreen> {
bool _autoStarted = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_autoStarted) return;
final mealType = GoRouterState.of(context).extra as String?;
if (mealType != null && mealType.isNotEmpty) {
_autoStarted = true;
// Defer to avoid calling context navigation during build.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_pickAndRecognize(context, _Mode.dish, mealType: mealType);
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -68,13 +42,6 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
onTap: () => _pickAndRecognize(context, _Mode.products),
),
const SizedBox(height: 16),
_ModeCard(
emoji: '🍽️',
title: 'Определить блюдо',
subtitle: 'КБЖУ≈ по фото готового блюда',
onTap: () => _pickAndRecognize(context, _Mode.dish),
),
const SizedBox(height: 16),
_ModeCard(
emoji: '✏️',
title: 'Добавить вручную',
@@ -88,9 +55,8 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
Future<void> _pickAndRecognize(
BuildContext context,
_Mode mode, {
String? mealType,
}) async {
_Mode mode,
) async {
final picker = ImagePicker();
List<XFile> files = [];
@@ -118,7 +84,7 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
}
if (!context.mounted) return;
final service = ref.read(_recognitionServiceProvider);
final service = ref.read(recognitionServiceProvider);
// Show loading overlay while the AI processes.
showDialog(
@@ -141,12 +107,6 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
Navigator.pop(context);
context.push('/scan/confirm', extra: items);
}
case _Mode.dish:
final dish = await service.recognizeDish(files.first);
if (context.mounted) {
Navigator.pop(context);
context.push('/scan/dish', extra: {'dish': dish, 'meal_type': mealType});
}
}
} catch (recognitionError) {
debugPrint('Recognition error: $recognitionError');
@@ -189,7 +149,7 @@ class _ScanScreenState extends ConsumerState<ScanScreen> {
// Mode enum
// ---------------------------------------------------------------------------
enum _Mode { receipt, products, dish }
enum _Mode { receipt, products }
// ---------------------------------------------------------------------------
// Widgets