diet-guard/app/lib/services/notification_backend_web.dart
2026-11-09 08:21:06 +01: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) 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(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();