mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
- FoodEntry.slot -> slotMinute: read via entrySlotMinute, written via slotFields (whole-hour stays byte-identical, else slot_min), kept by copyWithDeleted and the edit screen, so the phone never republishes a stripped copy of a PC entry. Out-of-range peer values echo, never throw. - Satisfaction is nearest-slot snapping (satisfiedSlots) everywhere: loggedSlotsToday, due check, kuchnia occupancy, future-day logged set (that day's schedule), slot selector. Schedule edits no longer un-log. - Future pre-log builds DateTime from slot ~/ 60, slot % 60. - Schedule history + sync go through scheduleToWire/scheduleFromWire. - Notifications: ids are slot minutes; syncToSlots cancels whatever the platform reports active (getActiveNotifications, reminder channel) that is not due, covering legacy hour ids; full 0..1439 sweep only if the lookup throws. - Settings: 15-minute dropdowns + "Custom..." (24h showTimePicker), off-grid values always selectable, counts capped by grid capacity; editor moved to widgets/meal_schedule_editor.dart + meal_time_dropdown.dart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015npsYVB2PdtWkG7MPAi4tn
154 lines
5.6 KiB
Dart
154 lines
5.6 KiB
Dart
/// Lets the user pre-log a meal against a future date + slot instead of now.
|
|
///
|
|
/// Its own file because `log_meal_screen.dart` is at the repo's 250-line cap.
|
|
///
|
|
/// Scoped to dates strictly after today: the date picker's `firstDate` is
|
|
/// tomorrow. Today's own not-yet-due slots keep using the screen's existing
|
|
/// [SlotSelectorRow] (driven by the real `DateTime.now()`), so this mixin
|
|
/// never has to reproduce its "due" (red) coloring, which is meaningless for
|
|
/// a day that hasn't started.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/models/food_entry.dart';
|
|
import 'package:diet_guard_app/models/local_time.dart';
|
|
import 'package:diet_guard_app/models/meal_schedule.dart';
|
|
import 'package:diet_guard_app/models/slot.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
|
import 'package:diet_guard_app/ui/theme.dart';
|
|
import 'package:diet_guard_app/widgets/slot_selector_row.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Adds a future-date meal-log picker to [LogMealScreen]'s state.
|
|
mixin LogMealFutureMixin<T extends StatefulWidget> on State<T> {
|
|
DateTime? _futureDate;
|
|
int? _futureSlot;
|
|
Set<int> _futureLoggedSlots = {};
|
|
MealSchedule? _futureSchedule;
|
|
|
|
/// The date currently chosen for a future log, or null when logging for
|
|
/// today through the screen's normal flow.
|
|
DateTime? get futureDate => _futureDate;
|
|
|
|
/// The slot chosen for [futureDate], or null until one is picked.
|
|
int? get futureSlot => _futureSlot;
|
|
|
|
/// Clears back to "log for today", dropping the picked date and slot.
|
|
void resetFutureLog() {
|
|
setState(() {
|
|
_futureDate = null;
|
|
_futureSlot = null;
|
|
_futureLoggedSlots = {};
|
|
_futureSchedule = null;
|
|
});
|
|
}
|
|
|
|
Future<void> _pickFutureDate() async {
|
|
final today = DateTime.now();
|
|
final tomorrow = DateTime(today.year, today.month, today.day + 1);
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
firstDate: tomorrow,
|
|
lastDate: tomorrow.add(const Duration(days: 365)),
|
|
initialDate: _futureDate ?? tomorrow,
|
|
);
|
|
if (picked == null || !mounted) return;
|
|
final log = await LogStorageService.instance.readLog();
|
|
if (!mounted) return;
|
|
final day = localDateKey(picked);
|
|
final schedule = MealScheduleService.history.forDay(day);
|
|
// Snapped against the schedule for *that* day, the same rule as
|
|
// `loggedSlotsToday`: an exact match would show a pre-logged meal's slot
|
|
// as open again after a schedule edit, inviting a duplicate.
|
|
final logged = satisfiedSlots([
|
|
for (final entry in log[day] ?? const <FoodEntry>[])
|
|
if (!entry.deleted) ?entry.slotMinute,
|
|
], schedule);
|
|
setState(() {
|
|
_futureDate = picked;
|
|
_futureSchedule = schedule;
|
|
_futureLoggedSlots = logged;
|
|
_futureSlot = null;
|
|
});
|
|
}
|
|
|
|
/// The "When" row: a "Log for later" button, or the picked date with a
|
|
/// button to change it and one to clear back to today.
|
|
Widget buildWhenRow(BuildContext context) {
|
|
final date = _futureDate;
|
|
final scheme = Theme.of(context).colorScheme;
|
|
return Row(
|
|
children: [
|
|
Icon(Icons.event, size: 18, color: scheme.onSurfaceVariant),
|
|
const SizedBox(width: AppSpacing.xs),
|
|
Expanded(child: Text(date == null ? 'Today' : localDateKey(date))),
|
|
TextButton.icon(
|
|
// Not Icons.calendar_month: the app bar's own "Calendar" nav button
|
|
// already uses that, and a second one on screen makes
|
|
// `find.byIcon` ambiguous for widget tests (and taps) alike.
|
|
icon: const Icon(Icons.date_range, size: 18),
|
|
label: Text(date == null ? 'Log for later' : 'Change date'),
|
|
onPressed: _pickFutureDate,
|
|
),
|
|
if (date != null)
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
tooltip: 'Back to today',
|
|
onPressed: resetFutureLog,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// The slot row to show: [futureDate]'s own (driven by that date's logged
|
|
/// slots) when a future date is picked, otherwise today's, built from the
|
|
/// screen's own state and [onTodaySlotSelected].
|
|
Widget buildSlotRow({
|
|
required Set<int> loggedSlots,
|
|
required int? selectedSlot,
|
|
required ValueChanged<int?> onTodaySlotSelected,
|
|
}) {
|
|
final date = _futureDate;
|
|
if (date == null) {
|
|
return SlotSelectorRow(
|
|
now: DateTime.now(),
|
|
loggedSlots: loggedSlots,
|
|
selectedSlot: selectedSlot,
|
|
onSlotSelected: onTodaySlotSelected,
|
|
);
|
|
}
|
|
return SlotSelectorRow(
|
|
now: DateTime(date.year, date.month, date.day),
|
|
schedule: _futureSchedule,
|
|
loggedSlots: _futureLoggedSlots,
|
|
selectedSlot: _futureSlot,
|
|
onSlotSelected: (slot) => setState(() => _futureSlot = slot),
|
|
);
|
|
}
|
|
|
|
/// Resolves the slot/time to log against: the future pick when one is
|
|
/// active, otherwise [todaySlot] for now. `error` is set instead when a
|
|
/// future date is chosen but no slot on it has been picked yet.
|
|
({String? error, int? slot, DateTime? when}) resolveLogTarget(
|
|
int? todaySlot,
|
|
) {
|
|
final date = _futureDate;
|
|
if (date == null) return (error: null, slot: todaySlot, when: null);
|
|
final slot = _futureSlot;
|
|
if (slot == null) {
|
|
return (
|
|
error: 'Pick a slot for that date first.',
|
|
slot: null,
|
|
when: null,
|
|
);
|
|
}
|
|
return (
|
|
error: null,
|
|
slot: slot,
|
|
// The slot is a minute of day, not an hour: passing it as the hour
|
|
// argument would roll 720 (12:00) forward a whole month.
|
|
when: DateTime(date.year, date.month, date.day, slot ~/ 60, slot % 60),
|
|
);
|
|
}
|
|
}
|