Files
HouseHoldKeaper/lib/features/settings/presentation/settings_screen.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

197 lines
6.4 KiB
Dart

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 ConsumerStatefulWidget {
const SettingsScreen({super.key});
@override
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
Future<void> _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<void> _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<void> _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: [
// Section 1: Appearance (Darstellung)
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
l10n.settingsSectionAppearance,
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.primary,
),
),
),
ListTile(
title: Text(l10n.settingsThemeLabel),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: SegmentedButton<ThemeMode>(
segments: [
ButtonSegment<ThemeMode>(
value: ThemeMode.system,
label: Text(l10n.themeSystem),
icon: const Icon(Icons.settings_suggest_outlined),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.light,
label: Text(l10n.themeLight),
icon: const Icon(Icons.light_mode_outlined),
),
ButtonSegment<ThemeMode>(
value: ThemeMode.dark,
label: Text(l10n.themeDark),
icon: const Icon(Icons.dark_mode_outlined),
),
],
selected: {currentThemeMode},
onSelectionChanged: (selection) {
ref
.read(themeProvider.notifier)
.setThemeMode(selection.first);
},
),
),
),
const Divider(indent: 16, endIndent: 16, height: 32),
// 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(
l10n.settingsSectionAbout,
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.primary,
),
),
),
ListTile(
title: Text(l10n.aboutAppName),
subtitle: Text(l10n.aboutTagline),
),
ListTile(
title: const Text('Version'),
subtitle: Text(l10n.aboutVersion('0.1.0')),
),
],
);
}
}