mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-12-07 21:52:35 +00:00
164 lines
5.1 KiB
Dart
164 lines
5.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:supplements/logging.dart';
|
|
|
|
import '../models/supplement.dart';
|
|
import '../providers/supplement_provider.dart';
|
|
import '../widgets/dialogs/bulk_take_dialog.dart';
|
|
import '../widgets/dialogs/take_supplement_dialog.dart';
|
|
|
|
/// Centralizes routing from notification actions/taps to in-app UI.
|
|
/// Handles both foreground/background taps and terminated-launch scenarios.
|
|
class NotificationRouter {
|
|
NotificationRouter._internal();
|
|
static final NotificationRouter instance = NotificationRouter._internal();
|
|
|
|
GlobalKey<NavigatorState>? _navigatorKey;
|
|
|
|
void initialize(GlobalKey<NavigatorState> navigatorKey) {
|
|
_navigatorKey = navigatorKey;
|
|
}
|
|
|
|
Future<void> handleNotificationResponse(NotificationResponse response) async {
|
|
final payloadMap = _decodePayload(response.payload);
|
|
final actionId = response.actionId;
|
|
printLog('🔔 handleNotificationResponse: actionId=$actionId payload=${response.payload} map=$payloadMap');
|
|
|
|
await _routeFromPayload(payloadMap);
|
|
}
|
|
|
|
Future<void> handleAppLaunchDetails(NotificationAppLaunchDetails? details) async {
|
|
if (details == null) return;
|
|
if (!details.didNotificationLaunchApp) return;
|
|
|
|
final resp = details.notificationResponse;
|
|
final payloadMap = _decodePayload(resp?.payload);
|
|
printLog('🚀 App launched from notification: payload=${resp?.payload} map=$payloadMap');
|
|
|
|
await _routeFromPayload(payloadMap);
|
|
}
|
|
|
|
Map<String, dynamic>? _decodePayload(String? payload) {
|
|
if (payload == null || payload.isEmpty) return null;
|
|
|
|
// Try JSON first
|
|
try {
|
|
final map = jsonDecode(payload);
|
|
if (map is Map<String, dynamic>) return map;
|
|
} catch (_) {
|
|
// Ignore and try fallback
|
|
}
|
|
|
|
// Fallback: previous implementation used HH:mm as raw payload
|
|
final hhmm = RegExp(r'^\d{2}:\d{2}$');
|
|
if (hhmm.hasMatch(payload)) {
|
|
return { 'type': 'group', 'time': payload };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Future<void> _routeFromPayload(Map<String, dynamic>? payload) async {
|
|
if (_navigatorKey == null) {
|
|
printLog('⚠️ NotificationRouter not initialized with navigatorKey');
|
|
return;
|
|
}
|
|
|
|
// Wait until navigator is ready and providers have loaded
|
|
final ready = await _waitUntilReady(timeout: const Duration(seconds: 5));
|
|
if (!ready) {
|
|
printLog('⚠️ Timeout waiting for app to be ready for routing');
|
|
return;
|
|
}
|
|
|
|
final context = _navigatorKey!.currentContext!;
|
|
final provider = context.read<SupplementProvider>();
|
|
|
|
if (payload == null) {
|
|
printLog('⚠️ No payload to route');
|
|
return;
|
|
}
|
|
|
|
final type = payload['type'];
|
|
if (type == 'single') {
|
|
final id = payload['id'];
|
|
if (id is int) {
|
|
Supplement? s;
|
|
try {
|
|
s = provider.supplements.firstWhere((el) => el.id == id);
|
|
} catch (_) {
|
|
s = null;
|
|
}
|
|
if (s == null) {
|
|
// Attempt reload once
|
|
await provider.loadSupplements();
|
|
try {
|
|
s = provider.supplements.firstWhere((el) => el.id == id);
|
|
} catch (_) {
|
|
s = null;
|
|
}
|
|
}
|
|
if (s != null) {
|
|
// For single: use the regular dialog (with time selection)
|
|
// Ensure we close any existing dialog first
|
|
_popAnyDialog(context);
|
|
await showTakeSupplementDialog(context, s, hideTime: false);
|
|
} else {
|
|
printLog('⚠️ Supplement id=$id not found for single-take routing');
|
|
_showSnack(context, 'Supplement not found');
|
|
}
|
|
}
|
|
} else if (type == 'group') {
|
|
final timeKey = payload['time'];
|
|
if (timeKey is String) {
|
|
// Build list of supplements scheduled at this timeKey
|
|
final List<Supplement> list = provider.supplements.where((s) {
|
|
return s.isActive && s.reminderTimes.contains(timeKey);
|
|
}).toList();
|
|
|
|
if (list.isEmpty) {
|
|
printLog('⚠️ No supplements found for group time=$timeKey');
|
|
_showSnack(context, 'No supplements for $timeKey');
|
|
return;
|
|
}
|
|
|
|
_popAnyDialog(context);
|
|
await showBulkTakeDialog(context, list);
|
|
}
|
|
} else {
|
|
printLog('⚠️ Unknown payload type: $type');
|
|
}
|
|
}
|
|
|
|
Future<bool> _waitUntilReady({required Duration timeout}) async {
|
|
final start = DateTime.now();
|
|
while (DateTime.now().difference(start) < timeout) {
|
|
final ctx = _navigatorKey!.currentContext;
|
|
if (ctx != null) {
|
|
final provider = Provider.of<SupplementProvider>(ctx, listen: false);
|
|
if (!provider.isLoading) {
|
|
return true;
|
|
}
|
|
}
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void _popAnyDialog(BuildContext context) {
|
|
if (Navigator.of(context, rootNavigator: true).canPop()) {
|
|
Navigator.of(context, rootNavigator: true).pop();
|
|
}
|
|
}
|
|
|
|
void _showSnack(BuildContext context, String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message)),
|
|
);
|
|
}
|
|
}
|