feat: dish recognition UX, background mode, and backend bug fixes
Flutter client: - Progress dialog: redesigned with pulsing animated icon, info hint about background mode, full-width Minimize button; dismiss signal via ValueNotifier so the dialog always closes regardless of widget lifecycle - Background recognition: when user taps Minimize, wasMinimizedByUser flag is set; on completion a snackbar is shown instead of opening DishResultSheet directly; snackbar action opens the sheet on demand - Fix dialog spinning forever: finally block guarantees dismissSignal=true on all exit paths including early returns from context.mounted checks - Fix DishResultSheet not appearing: add ValueKey to _DailyMealsSection and meal card Padding so Flutter reuses elements when _TodayJobsWidget is inserted/removed from the SliverChildListDelegate list - todayJobsProvider refresh: added refresh() method; called after job submit and on DishJobDone; all ref.read() calls guarded with context.mounted checks - food_search_sheet: scan buttons replaced with full-width stacked OutlinedButtons - app.dart: WidgetsBindingObserver refreshes scan providers on app resume - L10n: added dishRecognitionHint and minimize keys to all 12 locales Backend: - migrations/003: ALTER TYPE recipe_source ADD VALUE 'recommendation' to fix 22P02 error in GET /home/summary -> getRecommendations() - item_enricher: normalizeProductCategory() validates AI-returned category against known slugs, falls back to "other" — fixes products_category_fkey FK violation during receipt recognition - recognition prompt: enumerate valid categories so AI returns correct values Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
|
||||
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
|
||||
if (lifecycleState == AppLifecycleState.resumed) {
|
||||
ref.read(recentProductJobsProvider.notifier).refresh();
|
||||
ref.read(todayJobsProvider.notifier).load();
|
||||
ref.read(todayJobsProvider.notifier).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,24 +220,24 @@ class _FoodSearchSheetState extends ConsumerState<FoodSearchSheet> {
|
||||
),
|
||||
),
|
||||
|
||||
// Quick action chips
|
||||
// Scan action buttons — full-width, stacked vertically
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.onScanDish != null)
|
||||
ActionChip(
|
||||
avatar:
|
||||
const Icon(Icons.camera_alt_outlined, size: 18),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.camera_alt_outlined, size: 18),
|
||||
label: Text(l10n.scanDishPhoto),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onScanDish!();
|
||||
},
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.qr_code_scanner, size: 18),
|
||||
if (widget.onScanDish != null) const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.qr_code_scanner, size: 18),
|
||||
label: Text(l10n.scanBarcode),
|
||||
onPressed: _openBarcodeScanner,
|
||||
),
|
||||
|
||||
@@ -52,6 +52,8 @@ class TodayJobsNotifier
|
||||
state =
|
||||
await AsyncValue.guard(() => _service.listTodayUnlinkedJobs());
|
||||
}
|
||||
|
||||
Future<void> refresh() => load();
|
||||
}
|
||||
|
||||
final todayJobsProvider =
|
||||
|
||||
@@ -120,6 +120,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_DailyMealsSection(
|
||||
key: const ValueKey('daily_meals_section'),
|
||||
mealTypeIds: userMealTypes,
|
||||
entries: entries,
|
||||
dateString: dateString,
|
||||
@@ -777,6 +778,7 @@ class _DailyMealsSection extends ConsumerWidget {
|
||||
final String dateString;
|
||||
|
||||
const _DailyMealsSection({
|
||||
super.key,
|
||||
required this.mealTypeIds,
|
||||
required this.entries,
|
||||
required this.dateString,
|
||||
@@ -803,6 +805,7 @@ class _DailyMealsSection extends ConsumerWidget {
|
||||
.where((slot) => slot.mealType == mealTypeId)
|
||||
.toList();
|
||||
return Padding(
|
||||
key: ValueKey(mealTypeId),
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _MealCard(
|
||||
mealTypeOption: mealTypeOption,
|
||||
@@ -857,13 +860,19 @@ Future<void> _pickAndShowDishResult(
|
||||
if (image == null || !context.mounted) return;
|
||||
|
||||
// 3. Show progress dialog.
|
||||
// Capture root navigator before await to avoid GoRouter inner-navigator issues.
|
||||
final rootNavigator = Navigator.of(context, rootNavigator: true);
|
||||
// Use a dismiss signal so the dialog can close itself from within its own
|
||||
// context, avoiding GoRouter's inner-navigator pop() issues.
|
||||
final progressNotifier = _DishProgressNotifier(initialMessage: l10n.analyzingPhoto);
|
||||
final dismissSignal = ValueNotifier<bool>(false);
|
||||
bool wasMinimizedByUser = false;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => _DishProgressDialog(notifier: progressNotifier),
|
||||
builder: (_) => _DishProgressDialog(
|
||||
notifier: progressNotifier,
|
||||
dismissSignal: dismissSignal,
|
||||
onMinimize: () { wasMinimizedByUser = true; },
|
||||
),
|
||||
);
|
||||
|
||||
// 4. Determine target date and meal type for context.
|
||||
@@ -882,7 +891,9 @@ Future<void> _pickAndShowDishResult(
|
||||
targetDate: targetDate,
|
||||
targetMealType: resolvedMealType,
|
||||
);
|
||||
// Refresh immediately so the new queued job appears in the home screen list.
|
||||
if (!context.mounted) return;
|
||||
ref.read(todayJobsProvider.notifier).refresh();
|
||||
|
||||
await for (final event in service.streamJobEvents(jobCreated.jobId)) {
|
||||
if (!context.mounted) break;
|
||||
@@ -896,23 +907,52 @@ Future<void> _pickAndShowDishResult(
|
||||
case DishJobProcessing():
|
||||
progressNotifier.update(message: l10n.processing);
|
||||
case DishJobDone():
|
||||
rootNavigator.pop(); // close dialog
|
||||
dismissSignal.value = true;
|
||||
if (!context.mounted) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (sheetContext) => DishResultSheet(
|
||||
dish: event.result,
|
||||
preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null,
|
||||
jobId: jobCreated.jobId,
|
||||
targetDate: targetDate,
|
||||
onAdded: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
);
|
||||
ref.read(todayJobsProvider.notifier).refresh();
|
||||
if (wasMinimizedByUser) {
|
||||
// Recognition finished in background — notify without opening the sheet.
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.dishRecognized),
|
||||
action: SnackBarAction(
|
||||
label: l10n.addToJournal,
|
||||
onPressed: () {
|
||||
if (!context.mounted) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (sheetContext) => DishResultSheet(
|
||||
dish: event.result,
|
||||
preselectedMealType:
|
||||
mealTypeId.isNotEmpty ? mealTypeId : null,
|
||||
jobId: jobCreated.jobId,
|
||||
targetDate: targetDate,
|
||||
onAdded: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (sheetContext) => DishResultSheet(
|
||||
dish: event.result,
|
||||
preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null,
|
||||
jobId: jobCreated.jobId,
|
||||
targetDate: targetDate,
|
||||
onAdded: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
case DishJobFailed():
|
||||
rootNavigator.pop(); // close dialog
|
||||
dismissSignal.value = true;
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -929,11 +969,15 @@ Future<void> _pickAndShowDishResult(
|
||||
} catch (recognitionError) {
|
||||
debugPrint('Dish recognition error: $recognitionError');
|
||||
if (context.mounted) {
|
||||
rootNavigator.pop(); // close dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recognitionFailed)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
// Guarantee the dialog is always dismissed — covers early returns due to
|
||||
// context.mounted being false, stream ending without a terminal event, or
|
||||
// any unhandled exception. ValueNotifier deduplicates so double-setting is safe.
|
||||
dismissSignal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,39 +1009,203 @@ class _DishProgressNotifier extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class _DishProgressDialog extends StatelessWidget {
|
||||
class _DishProgressDialog extends StatefulWidget {
|
||||
final _DishProgressNotifier notifier;
|
||||
final ValueNotifier<bool> dismissSignal;
|
||||
|
||||
const _DishProgressDialog({required this.notifier});
|
||||
/// Called when the user explicitly closes the dialog via the Minimize button
|
||||
/// (i.e., before recognition has finished).
|
||||
final VoidCallback? onMinimize;
|
||||
|
||||
const _DishProgressDialog({
|
||||
required this.notifier,
|
||||
required this.dismissSignal,
|
||||
this.onMinimize,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DishProgressDialog> createState() => _DishProgressDialogState();
|
||||
}
|
||||
|
||||
class _DishProgressDialogState extends State<_DishProgressDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.dismissSignal.addListener(_onDismissSignal);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.dismissSignal.removeListener(_onDismissSignal);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDismissSignal() {
|
||||
if (widget.dismissSignal.value && mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) Navigator.of(context, rootNavigator: true).pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _minimize() {
|
||||
widget.onMinimize?.call();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ListenableBuilder(
|
||||
listenable: notifier,
|
||||
builder: (context, _) {
|
||||
final state = notifier.state;
|
||||
return AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message, textAlign: TextAlign.center),
|
||||
if (state.showUpgrade) ...[
|
||||
const SizedBox(height: 12),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 24),
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.notifier,
|
||||
builder: (_, __) {
|
||||
final state = widget.notifier.state;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const _PulsingRecognitionIcon(),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.upgradePrompt,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
state.message,
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (state.showUpgrade) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.upgradePrompt,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.dishRecognitionHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: _minimize,
|
||||
child: Text(l10n.minimize),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pulsing animated icon shown while dish recognition is in progress.
|
||||
class _PulsingRecognitionIcon extends StatefulWidget {
|
||||
const _PulsingRecognitionIcon();
|
||||
|
||||
@override
|
||||
State<_PulsingRecognitionIcon> createState() =>
|
||||
_PulsingRecognitionIconState();
|
||||
}
|
||||
|
||||
class _PulsingRecognitionIconState extends State<_PulsingRecognitionIcon>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _ringScale;
|
||||
late final Animation<double> _ringOpacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
)..repeat(reverse: true);
|
||||
_ringScale = Tween<double>(begin: 0.88, end: 1.12).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
_ringOpacity = Tween<double>(begin: 0.12, end: 0.45).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (_, __) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Transform.scale(
|
||||
scale: _ringScale.value,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primary
|
||||
.withValues(alpha: _ringOpacity.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primaryContainer,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.restaurant_outlined,
|
||||
size: 30,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "استبدال المنتج",
|
||||
"scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات"
|
||||
"scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات",
|
||||
"minimize": "تصغير",
|
||||
"dishRecognitionHint": "يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Produkt ersetzen",
|
||||
"scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm"
|
||||
"scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm",
|
||||
"minimize": "Minimieren",
|
||||
"dishRecognitionHint": "Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Replace product",
|
||||
"scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen"
|
||||
"scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen",
|
||||
"minimize": "Minimize",
|
||||
"dishRecognitionHint": "You can minimize — the result will appear on the Home screen when done"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Reemplazar producto",
|
||||
"scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos"
|
||||
"scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos",
|
||||
"minimize": "Minimizar",
|
||||
"dishRecognitionHint": "Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Remplacer le produit",
|
||||
"scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits"
|
||||
"scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits",
|
||||
"minimize": "Réduire",
|
||||
"dishRecognitionHint": "Vous pouvez réduire — le résultat apparaîtra sur l'écran d'accueil quand c'est terminé"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "उत्पाद बदलें",
|
||||
"scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा"
|
||||
"scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा",
|
||||
"minimize": "छोटा करें",
|
||||
"dishRecognitionHint": "आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Sostituisci prodotto",
|
||||
"scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti"
|
||||
"scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti",
|
||||
"minimize": "Riduci a icona",
|
||||
"dishRecognitionHint": "Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "商品を置き換える",
|
||||
"scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます"
|
||||
"scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます",
|
||||
"minimize": "最小化",
|
||||
"dishRecognitionHint": "最小化できます — 完了したら結果がホーム画面に表示されます"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "제품 교체",
|
||||
"scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다"
|
||||
"scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다",
|
||||
"minimize": "최소화",
|
||||
"dishRecognitionHint": "최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다"
|
||||
}
|
||||
|
||||
@@ -1215,6 +1215,18 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'You can close the app — this scan will appear in Recent Scans on the Products screen'**
|
||||
String get scanJobCloseHint;
|
||||
|
||||
/// No description provided for @minimize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Minimize'**
|
||||
String get minimize;
|
||||
|
||||
/// No description provided for @dishRecognitionHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can minimize — the result will appear on the Home screen when done'**
|
||||
String get dishRecognitionHint;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -575,4 +575,11 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات';
|
||||
|
||||
@override
|
||||
String get minimize => 'تصغير';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء';
|
||||
}
|
||||
|
||||
@@ -579,4 +579,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm';
|
||||
|
||||
@override
|
||||
String get minimize => 'Minimieren';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig';
|
||||
}
|
||||
|
||||
@@ -577,4 +577,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'You can close the app — this scan will appear in Recent Scans on the Products screen';
|
||||
|
||||
@override
|
||||
String get minimize => 'Minimize';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'You can minimize — the result will appear on the Home screen when done';
|
||||
}
|
||||
|
||||
@@ -580,4 +580,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos';
|
||||
|
||||
@override
|
||||
String get minimize => 'Minimizar';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo';
|
||||
}
|
||||
|
||||
@@ -580,4 +580,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Vous pouvez fermer l\'app — ce scan apparaîtra dans Scans récents sur l\'écran Produits';
|
||||
|
||||
@override
|
||||
String get minimize => 'Réduire';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Vous pouvez réduire — le résultat apparaîtra sur l\'écran d\'accueil quand c\'est terminé';
|
||||
}
|
||||
|
||||
@@ -578,4 +578,11 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा';
|
||||
|
||||
@override
|
||||
String get minimize => 'छोटा करें';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा';
|
||||
}
|
||||
|
||||
@@ -580,4 +580,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Puoi chiudere l\'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti';
|
||||
|
||||
@override
|
||||
String get minimize => 'Riduci a icona';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine';
|
||||
}
|
||||
|
||||
@@ -571,4 +571,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => 'アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます';
|
||||
|
||||
@override
|
||||
String get minimize => '最小化';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint => '最小化できます — 完了したら結果がホーム画面に表示されます';
|
||||
}
|
||||
|
||||
@@ -571,4 +571,10 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => '앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다';
|
||||
|
||||
@override
|
||||
String get minimize => '최소화';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint => '최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다';
|
||||
}
|
||||
|
||||
@@ -579,4 +579,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos';
|
||||
|
||||
@override
|
||||
String get minimize => 'Minimizar';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Você pode minimizar — o resultado aparecerá na tela inicial quando concluído';
|
||||
}
|
||||
|
||||
@@ -578,4 +578,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get scanJobCloseHint =>
|
||||
'Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов';
|
||||
|
||||
@override
|
||||
String get minimize => 'Свернуть';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint =>
|
||||
'Можно свернуть — результат появится на главном экране по завершении';
|
||||
}
|
||||
|
||||
@@ -570,4 +570,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanJobCloseHint => '您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中';
|
||||
|
||||
@override
|
||||
String get minimize => '最小化';
|
||||
|
||||
@override
|
||||
String get dishRecognitionHint => '可以最小化 — 完成后结果将显示在主屏幕上';
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Substituir produto",
|
||||
"scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos"
|
||||
"scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos",
|
||||
"minimize": "Minimizar",
|
||||
"dishRecognitionHint": "Você pode minimizar — o resultado aparecerá na tela inicial quando concluído"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "Заменить продукт",
|
||||
"scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов"
|
||||
"scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов",
|
||||
"minimize": "Свернуть",
|
||||
"dishRecognitionHint": "Можно свернуть — результат появится на главном экране по завершении"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,7 @@
|
||||
}
|
||||
},
|
||||
"recognitionReplaceProduct": "替换产品",
|
||||
"scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中"
|
||||
"scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中",
|
||||
"minimize": "最小化",
|
||||
"dishRecognitionHint": "可以最小化 — 完成后结果将显示在主屏幕上"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user