diet-guard/app/lib/services/notification_backend_web.dart
Krzysztof kuhy Rudnicki f83938bfe3 app: minute-of-day slots (phase 2 of 3: every app consumer)
- 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
2026-10-10 19:55:26 +02:00

64 lines
2.2 KiB
Dart

/// Browser-Notifications-API [NotificationBackend] for the desktop app.
///
/// A browser notification only exists while the page that raised it is open,
/// so this is a genuinely weaker surface than Android's: closing the desktop
/// window drops every pending reminder. That is accepted -- the PC's real
/// backstop is `diet-guard-gate.timer`, which locks the screen rather than
/// asking politely.
library;
import 'dart:js_interop';
import 'package:diet_guard_app/services/notification_backend.dart';
import 'package:web/web.dart' as web;
/// [NotificationBackend] over the browser's `Notification` API.
class WebNotificationBackend implements NotificationBackend {
/// Live notifications by slot, so [cancel] can close the right one. The
/// `tag` also makes the browser replace rather than stack a re-shown slot;
/// the map is what makes an explicit close possible.
final _shown = <int, web.Notification>{};
bool _granted = false;
@override
Future<void> initialize() async {}
@override
Future<bool?> requestPermission() async {
if (web.Notification.permission == 'granted') {
_granted = true;
return true;
}
if (web.Notification.permission == 'denied') return false;
final result = await web.Notification.requestPermission().toDart;
return _granted = result.toDart == 'granted';
}
@override
Future<void> show(int slot, String title, String body) async {
// Posting without permission throws; a silent no-op matches this
// service's degrade-quietly stance everywhere else.
if (!_granted) return;
_shown[slot] = web.Notification(
title,
web.NotificationOptions(body: body, tag: 'diet_guard_slot_$slot'),
);
}
@override
Future<void> cancel(int slot) async {
_shown.remove(slot)?.close();
}
/// The page's own live notifications. Complete for this surface: a browser
/// notification cannot outlive the page that raised it in any way this
/// page could still close, so nothing posted by an earlier load (or an
/// older, hour-keyed build) is reachable to orphan.
@override
Future<Set<int>> activeIds() async => _shown.keys.toSet();
}
/// Opens the platform notification backend.
NotificationBackend openNotificationBackend() => WebNotificationBackend();