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>
101 lines
3.4 KiB
Dart
101 lines
3.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../features/scan/recognition_service.dart';
|
|
import '../../shared/models/home_summary.dart';
|
|
import '../../shared/models/menu.dart';
|
|
import '../menu/menu_provider.dart';
|
|
import 'home_service.dart';
|
|
|
|
// ── Selected date (persists while app is open) ────────────────
|
|
|
|
/// The date currently viewed on the home screen.
|
|
/// Defaults to today; can be changed via the date selector.
|
|
final selectedDateProvider = StateProvider<DateTime>((ref) => DateTime.now());
|
|
|
|
/// Formats a [DateTime] to the 'YYYY-MM-DD' string expected by the diary API.
|
|
String formatDateForDiary(DateTime date) =>
|
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-'
|
|
'${date.day.toString().padLeft(2, '0')}';
|
|
|
|
// ── Home summary ──────────────────────────────────────────────
|
|
|
|
class HomeNotifier extends StateNotifier<AsyncValue<HomeSummary>> {
|
|
final HomeService _service;
|
|
|
|
HomeNotifier(this._service) : super(const AsyncValue.loading()) {
|
|
load();
|
|
}
|
|
|
|
Future<void> load() async {
|
|
state = const AsyncValue.loading();
|
|
state = await AsyncValue.guard(() => _service.getSummary());
|
|
}
|
|
}
|
|
|
|
final homeProvider =
|
|
StateNotifierProvider<HomeNotifier, AsyncValue<HomeSummary>>(
|
|
(ref) => HomeNotifier(ref.read(homeServiceProvider)),
|
|
);
|
|
|
|
// ── Today's unlinked recognition jobs ─────────────────────────
|
|
|
|
class TodayJobsNotifier
|
|
extends StateNotifier<AsyncValue<List<DishJobSummary>>> {
|
|
final RecognitionService _service;
|
|
|
|
TodayJobsNotifier(this._service) : super(const AsyncValue.loading()) {
|
|
load();
|
|
}
|
|
|
|
Future<void> load() async {
|
|
state = const AsyncValue.loading();
|
|
state =
|
|
await AsyncValue.guard(() => _service.listTodayUnlinkedJobs());
|
|
}
|
|
|
|
Future<void> refresh() => load();
|
|
}
|
|
|
|
final todayJobsProvider =
|
|
StateNotifierProvider<TodayJobsNotifier, AsyncValue<List<DishJobSummary>>>(
|
|
(ref) => TodayJobsNotifier(ref.read(recognitionServiceProvider)),
|
|
);
|
|
|
|
// ── All recognition jobs (history screen) ─────────────────────
|
|
|
|
class AllJobsNotifier extends StateNotifier<AsyncValue<List<DishJobSummary>>> {
|
|
final RecognitionService _service;
|
|
|
|
AllJobsNotifier(this._service) : super(const AsyncValue.loading()) {
|
|
load();
|
|
}
|
|
|
|
Future<void> load() async {
|
|
state = const AsyncValue.loading();
|
|
state = await AsyncValue.guard(() => _service.listAllJobs());
|
|
}
|
|
}
|
|
|
|
final allJobsProvider =
|
|
StateNotifierProvider<AllJobsNotifier, AsyncValue<List<DishJobSummary>>>(
|
|
(ref) => AllJobsNotifier(ref.read(recognitionServiceProvider)),
|
|
);
|
|
|
|
// ── Planned meals from menu ────────────────────────────────────
|
|
|
|
/// Returns planned [MealSlot]s from the menu plan for [dateString].
|
|
/// Derives from the already-cached weekly menu — no extra network call.
|
|
/// Returns an empty list when no plan exists for that week.
|
|
final plannedMealsProvider =
|
|
Provider.family<List<MealSlot>, String>((ref, dateString) {
|
|
final date = DateTime.parse(dateString);
|
|
final weekString = isoWeekString(date);
|
|
final menuState = ref.watch(menuProvider(weekString));
|
|
final plan = menuState.valueOrNull;
|
|
if (plan == null) return [];
|
|
for (final day in plan.days) {
|
|
if (day.date == dateString) return day.meals;
|
|
}
|
|
return [];
|
|
});
|