diff --git a/lib/core/notifications/notification_service.dart b/lib/core/notifications/notification_service.dart index 864f9d2..ac2da81 100644 --- a/lib/core/notifications/notification_service.dart +++ b/lib/core/notifications/notification_service.dart @@ -3,6 +3,8 @@ 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; @@ -74,6 +76,6 @@ class NotificationService { } static void _onTap(NotificationResponse response) { - // Navigation to Home tab — wired in Plan 02 via global navigator key + router.go('/'); } } diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 9d5db40..5079aad 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -1,17 +1,101 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:household_keeper/core/notifications/notification_service.dart'; +import 'package:household_keeper/core/notifications/notification_settings_notifier.dart'; +import 'package:household_keeper/core/providers/database_provider.dart'; import 'package:household_keeper/core/theme/theme_provider.dart'; +import 'package:household_keeper/features/home/data/daily_plan_dao.dart'; import 'package:household_keeper/l10n/app_localizations.dart'; -class SettingsScreen extends ConsumerWidget { +class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends ConsumerState { + Future _onNotificationToggle(bool value) async { + if (value) { + // Enabling: request permission first + final granted = await NotificationService().requestPermission(); + if (!granted) { + // Permission denied — show SnackBar to guide user to system settings + if (!mounted) return; + final l10n = AppLocalizations.of(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.notificationsPermissionDeniedHint), + ), + ); + // Toggle stays OFF — do not update state + return; + } + // Permission granted: enable and schedule + await ref + .read(notificationSettingsProvider.notifier) + .setEnabled(true); + await _scheduleNotification(); + } else { + // Disabling: update state and cancel + await ref + .read(notificationSettingsProvider.notifier) + .setEnabled(false); + await NotificationService().cancelAll(); + } + } + + Future _scheduleNotification() async { + final settings = ref.read(notificationSettingsProvider); + if (!settings.enabled) return; + + final db = ref.read(appDatabaseProvider); + final dao = DailyPlanDao(db); + final total = await dao.getOverdueAndTodayTaskCount(); + final overdue = await dao.getOverdueTaskCount(); + + if (total == 0) { + // No tasks today — skip notification + await NotificationService().cancelAll(); + return; + } + + if (!mounted) return; + final l10n = AppLocalizations.of(context); + final body = overdue > 0 + ? l10n.notificationBodyWithOverdue(total, overdue) + : l10n.notificationBody(total); + final title = l10n.notificationTitle; + + await NotificationService().scheduleDailyNotification( + time: settings.time, + title: title, + body: body, + ); + } + + Future _onPickTime() async { + final settings = ref.read(notificationSettingsProvider); + final picked = await showTimePicker( + context: context, + initialTime: settings.time, + initialEntryMode: TimePickerEntryMode.dial, + ); + if (picked != null) { + await ref + .read(notificationSettingsProvider.notifier) + .setTime(picked); + await _scheduleNotification(); + } + } + + @override + Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final currentThemeMode = ref.watch(themeProvider); + final notificationSettings = ref.watch(notificationSettingsProvider); return ListView( children: [ @@ -59,7 +143,36 @@ class SettingsScreen extends ConsumerWidget { const Divider(indent: 16, endIndent: 16, height: 32), - // Section 2: About (Ueber) + // Section 2: Notifications (Benachrichtigungen) + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text( + l10n.settingsSectionNotifications, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + SwitchListTile( + title: Text(l10n.notificationsEnabledLabel), + value: notificationSettings.enabled, + onChanged: _onNotificationToggle, + ), + // Progressive disclosure: time picker only when enabled + AnimatedSize( + duration: const Duration(milliseconds: 200), + child: notificationSettings.enabled + ? ListTile( + title: Text(l10n.notificationsTimeLabel), + trailing: Text(notificationSettings.time.format(context)), + onTap: _onPickTime, + ) + : const SizedBox.shrink(), + ), + + const Divider(indent: 16, endIndent: 16, height: 32), + + // Section 3: About (Ueber) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text( diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e319ac6..8094718 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -471,6 +471,48 @@ abstract class AppLocalizations { /// In de, this message translates to: /// **'Noch keine Aufgaben angelegt'** String get dailyPlanNoTasks; + + /// No description provided for @settingsSectionNotifications. + /// + /// In de, this message translates to: + /// **'Benachrichtigungen'** + String get settingsSectionNotifications; + + /// No description provided for @notificationsEnabledLabel. + /// + /// In de, this message translates to: + /// **'Tägliche Erinnerung'** + String get notificationsEnabledLabel; + + /// No description provided for @notificationsTimeLabel. + /// + /// In de, this message translates to: + /// **'Uhrzeit'** + String get notificationsTimeLabel; + + /// No description provided for @notificationsPermissionDeniedHint. + /// + /// In de, this message translates to: + /// **'Benachrichtigungen sind in den Systemeinstellungen deaktiviert. Tippe hier, um sie zu aktivieren.'** + String get notificationsPermissionDeniedHint; + + /// No description provided for @notificationTitle. + /// + /// In de, this message translates to: + /// **'Dein Tagesplan'** + String get notificationTitle; + + /// No description provided for @notificationBody. + /// + /// In de, this message translates to: + /// **'{count} Aufgaben fällig'** + String notificationBody(int count); + + /// No description provided for @notificationBodyWithOverdue. + /// + /// In de, this message translates to: + /// **'{count} Aufgaben fällig ({overdue} überfällig)'** + String notificationBodyWithOverdue(int count, int overdue); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1a99a3d..e063302 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -210,4 +210,30 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dailyPlanNoTasks => 'Noch keine Aufgaben angelegt'; + + @override + String get settingsSectionNotifications => 'Benachrichtigungen'; + + @override + String get notificationsEnabledLabel => 'Tägliche Erinnerung'; + + @override + String get notificationsTimeLabel => 'Uhrzeit'; + + @override + String get notificationsPermissionDeniedHint => + 'Benachrichtigungen sind in den Systemeinstellungen deaktiviert. Tippe hier, um sie zu aktivieren.'; + + @override + String get notificationTitle => 'Dein Tagesplan'; + + @override + String notificationBody(int count) { + return '$count Aufgaben fällig'; + } + + @override + String notificationBodyWithOverdue(int count, int overdue) { + return '$count Aufgaben fällig ($overdue überfällig)'; + } }