mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 14:27:39 +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
119 lines
5.0 KiB
Dart
119 lines
5.0 KiB
Dart
/// A single row that both shows today's slot status (logged/due/upcoming)
|
|
/// and lets the user pick which slot they're logging for, replacing what
|
|
/// used to be three separate stacked elements.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/models/meal_schedule.dart';
|
|
import 'package:diet_guard_app/models/slot.dart';
|
|
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
|
import 'package:diet_guard_app/ui/theme.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// One row of [ChoiceChip]s, one per today's slot (labelled `HH:MM`). Each
|
|
/// chip is simultaneously selectable (tap to log for that slot) and
|
|
/// status-colored (green+check = logged, red = due, grey = upcoming), so no
|
|
/// separate status bar or caption text is needed.
|
|
///
|
|
/// There used to be a fifth "Snack" chip that selected no slot at all,
|
|
/// removed 2026-08-14. Entries with a null slot still exist in storage and
|
|
/// still arrive over sync, so the nullable types below are deliberate -- but
|
|
/// this widget no longer produces one.
|
|
class SlotSelectorRow extends StatelessWidget {
|
|
/// Creates a [SlotSelectorRow].
|
|
const SlotSelectorRow({
|
|
required this.now,
|
|
required this.loggedSlots,
|
|
required this.selectedSlot,
|
|
required this.onSlotSelected,
|
|
this.schedule,
|
|
super.key,
|
|
});
|
|
|
|
/// Reference time used to decide which slots are due.
|
|
final DateTime now;
|
|
|
|
/// The schedule whose slots to show; defaults to the one in force today.
|
|
///
|
|
/// Passed explicitly for a future day so the chips and [loggedSlots] (which
|
|
/// the caller snapped against that day's schedule) can never disagree.
|
|
final MealSchedule? schedule;
|
|
|
|
/// Slot minutes already satisfied by the day's log (`satisfiedSlots`).
|
|
final Set<int> loggedSlots;
|
|
|
|
/// The slot currently chosen to log for. Nullable for historical reasons
|
|
/// (see the class doc); no chip in this row selects null any more.
|
|
final int? selectedSlot;
|
|
|
|
/// Called with the tapped slot's minute. Never called with null.
|
|
final ValueChanged<int?> onSlotSelected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
// A `!` here would crash in any context that doesn't build its theme
|
|
// from buildAppTheme() -- most widget tests just wrap in a bare
|
|
// MaterialApp(home: ...). Falling back to the app's own dark instance
|
|
// matches production exactly when the extension is genuinely absent.
|
|
final statusColors =
|
|
Theme.of(context).extension<AppStatusColors>() ?? AppStatusColors.dark;
|
|
final schedule = this.schedule ?? MealScheduleService.current;
|
|
final elapsed = elapsedSlots(now, schedule).toSet();
|
|
// One row, always. A Wrap dropped the later pills onto a second line as
|
|
// soon as the row outgrew the width, which it does even at four: a logged
|
|
// chip measures ~124px at Material defaults, so four already need 522px
|
|
// against ~330px of phone width.
|
|
//
|
|
// FittedBox scales the whole row down uniformly instead. A Row of
|
|
// Expanded children also avoids the wrap, but imposes a *tight* width on
|
|
// each chip, and a Chip narrower than avatar+padding+label clips its label
|
|
// rather than shrinking -- measured at 10px per label, about one
|
|
// character, and it throws no overflow error while doing it.
|
|
return FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerLeft,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
spacing: AppSpacing.xs + 2,
|
|
children: [
|
|
...daySlots(schedule).map((slot) {
|
|
final isLogged = loggedSlots.contains(slot);
|
|
final isDue = !isLogged && elapsed.contains(slot);
|
|
final color = isLogged
|
|
? statusColors.success
|
|
: isDue
|
|
? scheme.error
|
|
: scheme.onSurfaceVariant;
|
|
final isSelected = selectedSlot == slot;
|
|
return ChoiceChip(
|
|
label: Text(slotLabel(slot)),
|
|
selected: isSelected,
|
|
// Icon reads lighter than the label text (rule 28): reduced
|
|
// opacity instead of the label's full-strength color.
|
|
avatar: isLogged
|
|
? Icon(
|
|
Icons.check,
|
|
size: 14,
|
|
color: color.withValues(alpha: 0.72),
|
|
)
|
|
: null,
|
|
backgroundColor: color.withValues(alpha: 0.15),
|
|
selectedColor: color.withValues(alpha: 0.35),
|
|
labelStyle: TextStyle(color: color),
|
|
side: BorderSide(
|
|
width: isSelected ? 2 : 1,
|
|
color: isSelected ? color : color.withValues(alpha: 0.4),
|
|
),
|
|
// Trim the Material defaults so the row needs less scaling down:
|
|
// every pixel saved here is legibility kept at six meals.
|
|
visualDensity: VisualDensity.compact,
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
onSelected: (_) => onSlotSelected(slot),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|