Files
HouseHoldKeaper/lib/core/notifications/notification_service.dart
Jean-Luc Makiola 0103ddebbb feat(04-02): wire notification settings UI, permission flow, scheduling, and tap navigation
- Convert SettingsScreen from ConsumerWidget to ConsumerStatefulWidget
- Add Benachrichtigungen section between Darstellung and Uber sections
- SwitchListTile with permission request on toggle ON (Android 13+)
- Toggle reverts to OFF on permission denial with SnackBar hint
- AnimatedSize progressive disclosure for time picker row when enabled
- _scheduleNotification() queries DailyPlanDao for task/overdue counts
- Skip notification scheduling when task count is 0
- Notification body includes overdue split when overdue > 0
- _onPickTime() shows Material 3 showTimePicker dialog then reschedules
- Wire router.go('/') in NotificationService._onTap for tap navigation
- Regenerate AppLocalizations with 7 new notification strings from Plan 01 ARB
2026-03-16 15:06:00 +01:00

82 lines
2.5 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show TimeOfDay;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:household_keeper/core/router/router.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final _plugin = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(android: android);
await _plugin.initialize(
settings: settings,
onDidReceiveNotificationResponse: _onTap,
);
}
Future<bool> requestPermission() async {
final android = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
return await android?.requestNotificationsPermission() ?? false;
}
Future<void> scheduleDailyNotification({
required TimeOfDay time,
required String title,
required String body,
}) async {
await _plugin.cancelAll();
final scheduledDate = nextInstanceOf(time);
const details = NotificationDetails(
android: AndroidNotificationDetails(
'daily_summary',
'Tägliche Zusammenfassung',
channelDescription: 'Tägliche Aufgaben-Erinnerung',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
),
);
await _plugin.zonedSchedule(
id: 0,
title: title,
body: body,
scheduledDate: scheduledDate,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
}
Future<void> cancelAll() => _plugin.cancelAll();
/// Computes the next occurrence of [time] as a [tz.TZDateTime].
/// Returns today if [time] is still in the future, tomorrow otherwise.
@visibleForTesting
tz.TZDateTime nextInstanceOf(TimeOfDay time) {
final now = tz.TZDateTime.now(tz.local);
var scheduled = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
time.hour,
time.minute,
);
if (scheduled.isBefore(now)) {
scheduled = scheduled.add(const Duration(days: 1));
}
return scheduled;
}
static void _onTap(NotificationResponse response) {
router.go('/');
}
}