mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 18:13:55 +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
64 lines
2.2 KiB
Dart
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();
|