- 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
197 lines
6.4 KiB
Dart
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')),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|