From 4aa31a2e442248a9f1994cf709747e06a9d1ebb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 12 Jul 2026 23:41:12 +0200 Subject: [PATCH] extract SettingsCheckboxTile for settings toggles --- .../settings/sections/dev_tools_section.dart | 67 +++++------ .../settings/sections/files_section.dart | 38 +++---- .../pages/settings/sections/talk_section.dart | 104 ++++++++---------- .../settings/sections/timetable_section.dart | 22 ++-- .../widgets/settings_checkbox_tile.dart | 42 +++++++ 5 files changed, 133 insertions(+), 140 deletions(-) create mode 100644 lib/view/pages/settings/widgets/settings_checkbox_tile.dart diff --git a/lib/view/pages/settings/sections/dev_tools_section.dart b/lib/view/pages/settings/sections/dev_tools_section.dart index 9ed675d..a81eeab 100644 --- a/lib/view/pages/settings/sections/dev_tools_section.dart +++ b/lib/view/pages/settings/sections/dev_tools_section.dart @@ -6,13 +6,13 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../storage/settings.dart' as model; -import '../../../../utils/haptics.dart'; import '../../../../widget/centered_leading.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/debug/cache_view.dart'; import '../../../../widget/debug/json_viewer.dart'; import '../../../../widget/details_bottom_sheet.dart'; import '../widgets/endpoint_picker.dart'; +import '../widgets/settings_checkbox_tile.dart'; class DevToolsSection extends StatefulWidget { final SettingsCubit settings; @@ -41,49 +41,32 @@ class _DevToolsSectionState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: const Icon(Icons.auto_graph_outlined), - title: const Text('Performance graph'), - trailing: Checkbox( - value: dev.showPerformanceOverlay, - onChanged: (e) { - Haptics.selection(); - widget.settings - .val(write: true) - .devToolsSettings - .showPerformanceOverlay = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.auto_graph_outlined, + title: 'Performance graph', + value: dev.showPerformanceOverlay, + onChanged: (e) => widget.settings + .val(write: true) + .devToolsSettings + .showPerformanceOverlay = e, ), - ListTile( - leading: const Icon( - Icons.screen_search_desktop_outlined, - ), - title: const Text('Indicate offscreen layers'), - trailing: Checkbox( - value: dev.checkerboardOffscreenLayers, - onChanged: (e) { - Haptics.selection(); - widget.settings - .val(write: true) - .devToolsSettings - .checkerboardOffscreenLayers = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.screen_search_desktop_outlined, + title: 'Indicate offscreen layers', + value: dev.checkerboardOffscreenLayers, + onChanged: (e) => widget.settings + .val(write: true) + .devToolsSettings + .checkerboardOffscreenLayers = e, ), - ListTile( - leading: const Icon(Icons.imagesearch_roller_outlined), - title: const Text('Indicate raster cache images'), - trailing: Checkbox( - value: dev.checkerboardRasterCacheImages, - onChanged: (e) { - Haptics.selection(); - widget.settings - .val(write: true) - .devToolsSettings - .checkerboardRasterCacheImages = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.imagesearch_roller_outlined, + title: 'Indicate raster cache images', + value: dev.checkerboardRasterCacheImages, + onChanged: (e) => widget.settings + .val(write: true) + .devToolsSettings + .checkerboardRasterCacheImages = e, ), ], ); diff --git a/lib/view/pages/settings/sections/files_section.dart b/lib/view/pages/settings/sections/files_section.dart index 9853b1f..159073b 100644 --- a/lib/view/pages/settings/sections/files_section.dart +++ b/lib/view/pages/settings/sections/files_section.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../../utils/haptics.dart'; +import '../widgets/settings_checkbox_tile.dart'; class FilesSection extends StatelessWidget { const FilesSection({super.key}); @@ -12,30 +12,20 @@ class FilesSection extends StatelessWidget { final settings = context.watch(); return Column( children: [ - ListTile( - leading: const Icon(Icons.drive_folder_upload_outlined), - title: const Text('Ordner in Dateien nach oben sortieren'), - trailing: Checkbox( - value: settings.val().fileSettings.sortFoldersToTop, - onChanged: (e) { - Haptics.selection(); - settings.val(write: true).fileSettings.sortFoldersToTop = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.drive_folder_upload_outlined, + title: 'Ordner in Dateien nach oben sortieren', + value: settings.val().fileSettings.sortFoldersToTop, + onChanged: (e) => + settings.val(write: true).fileSettings.sortFoldersToTop = e, ), - ListTile( - leading: const Icon(Icons.open_in_new_outlined), - title: const Text('Dateien immer mit Systemdialog öffnen'), - trailing: Checkbox( - value: settings.val().fileViewSettings.alwaysOpenExternally, - onChanged: (e) { - Haptics.selection(); - settings - .val(write: true) - .fileViewSettings - .alwaysOpenExternally = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.open_in_new_outlined, + title: 'Dateien immer mit Systemdialog öffnen', + value: settings.val().fileViewSettings.alwaysOpenExternally, + onChanged: (e) => + settings.val(write: true).fileViewSettings.alwaysOpenExternally = + e, ), ], ); diff --git a/lib/view/pages/settings/sections/talk_section.dart b/lib/view/pages/settings/sections/talk_section.dart index 50b50df..e6d5dd2 100644 --- a/lib/view/pages/settings/sections/talk_section.dart +++ b/lib/view/pages/settings/sections/talk_section.dart @@ -6,9 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../push/push_registration.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../../utils/haptics.dart'; import '../../../../widget/centered_leading.dart'; import '../widgets/push_status_sheet.dart'; +import '../widgets/settings_checkbox_tile.dart'; class TalkSection extends StatelessWidget { const TalkSection({super.key}); @@ -20,27 +20,19 @@ class TalkSection extends StatelessWidget { final notificationSettings = settings.val().notificationSettings; return Column( children: [ - ListTile( - leading: const Icon(Icons.star_border), - title: const Text('Favoriten im Talk nach oben sortieren'), - trailing: Checkbox( - value: talkSettings.sortFavoritesToTop, - onChanged: (e) { - Haptics.selection(); - settings.val(write: true).talkSettings.sortFavoritesToTop = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.star_border, + title: 'Favoriten im Talk nach oben sortieren', + value: talkSettings.sortFavoritesToTop, + onChanged: (e) => + settings.val(write: true).talkSettings.sortFavoritesToTop = e, ), - ListTile( - leading: const Icon(Icons.mark_email_unread_outlined), - title: const Text('Ungelesene Chats nach oben sortieren'), - trailing: Checkbox( - value: talkSettings.sortUnreadToTop, - onChanged: (e) { - Haptics.selection(); - settings.val(write: true).talkSettings.sortUnreadToTop = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.mark_email_unread_outlined, + title: 'Ungelesene Chats nach oben sortieren', + value: talkSettings.sortUnreadToTop, + onChanged: (e) => + settings.val(write: true).talkSettings.sortUnreadToTop = e, ), ListTile( leading: const Icon(Icons.wallpaper_outlined), @@ -49,46 +41,38 @@ class TalkSection extends StatelessWidget { trailing: const Icon(Icons.arrow_right), onTap: () => AppRoutes.openChatBackgroundSettings(context), ), - ListTile( - leading: const CenteredLeading( - Icon(Icons.notifications_active_outlined), - ), - title: const Text('Push-Benachrichtigungen'), - subtitle: const Text( - 'Benachrichtigungen bei neuen Talk-Nachrichten erhalten', - ), - trailing: Checkbox( - value: notificationSettings.enabled, - onChanged: (e) { - Haptics.selection(); - final enabled = e ?? false; - settings.val(write: true).notificationSettings.enabled = enabled; - // Turning off does NOT unregister: the device stays subscribed so - // silent sync pushes keep arriving; the message handler and iOS - // NSE suppress only the visible notification (via the mirrored - // flag). Enabling (re-)registers and ensures the OS permission. - if (enabled) { - final messenger = ScaffoldMessenger.of(context); - unawaited(() async { - // Only register when the OS permission isn't explicitly - // denied — otherwise NC + proxy would push into the void. - if (await PushRegistration.requestOsPermission()) { - await PushRegistration().register(); - } else { - messenger.showSnackBar( - const SnackBar( - content: Text( - 'Die Benachrichtigungsberechtigung wurde in den ' - 'Systemeinstellungen deaktiviert. Bitte aktiviere ' - 'sie dort, um Push-Benachrichtigungen zu erhalten.', - ), + SettingsCheckboxTile( + icon: Icons.notifications_active_outlined, + title: 'Push-Benachrichtigungen', + subtitle: 'Benachrichtigungen bei neuen Talk-Nachrichten erhalten', + value: notificationSettings.enabled, + onChanged: (enabled) { + settings.val(write: true).notificationSettings.enabled = enabled; + // Turning off does NOT unregister: the device stays subscribed so + // silent sync pushes keep arriving; the message handler and iOS + // NSE suppress only the visible notification (via the mirrored + // flag). Enabling (re-)registers and ensures the OS permission. + if (enabled) { + final messenger = ScaffoldMessenger.of(context); + unawaited(() async { + // Only register when the OS permission isn't explicitly + // denied — otherwise NC + proxy would push into the void. + if (await PushRegistration.requestOsPermission()) { + await PushRegistration().register(); + } else { + messenger.showSnackBar( + const SnackBar( + content: Text( + 'Die Benachrichtigungsberechtigung wurde in den ' + 'Systemeinstellungen deaktiviert. Bitte aktiviere ' + 'sie dort, um Push-Benachrichtigungen zu erhalten.', ), - ); - } - }()); - } - }, - ), + ), + ); + } + }()); + } + }, ), ListTile( leading: const CenteredLeading(Icon(Icons.monitor_heart_outlined)), diff --git a/lib/view/pages/settings/sections/timetable_section.dart b/lib/view/pages/settings/sections/timetable_section.dart index f5ddfc6..315031c 100644 --- a/lib/view/pages/settings/sections/timetable_section.dart +++ b/lib/view/pages/settings/sections/timetable_section.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; -import '../../../../utils/haptics.dart'; import '../../../../view/pages/timetable/data/timetable_name_mode.dart'; +import '../widgets/settings_checkbox_tile.dart'; class TimetableSection extends StatelessWidget { const TimetableSection({super.key}); @@ -43,19 +43,13 @@ class TimetableSection extends StatelessWidget { value!, ), ), - ListTile( - leading: const Icon(Icons.calendar_view_day_outlined), - title: const Text('Doppelstunden zusammenhängend anzeigen'), - trailing: Checkbox( - value: timetableSettings.connectDoubleLessons, - onChanged: (e) { - Haptics.selection(); - settings - .val(write: true) - .timetableSettings - .connectDoubleLessons = e!; - }, - ), + SettingsCheckboxTile( + icon: Icons.calendar_view_day_outlined, + title: 'Doppelstunden zusammenhängend anzeigen', + value: timetableSettings.connectDoubleLessons, + onChanged: (e) => + settings.val(write: true).timetableSettings.connectDoubleLessons = + e, ), ListTile( leading: const Icon(Icons.palette_outlined), diff --git a/lib/view/pages/settings/widgets/settings_checkbox_tile.dart b/lib/view/pages/settings/widgets/settings_checkbox_tile.dart new file mode 100644 index 0000000..724dca8 --- /dev/null +++ b/lib/view/pages/settings/widgets/settings_checkbox_tile.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import '../../../../utils/haptics.dart'; +import '../../../../widget/centered_leading.dart'; + +/// Settings row with a trailing checkbox. Fires [Haptics.selection] before +/// invoking [onChanged] (with the resolved non-null value), so the sections +/// don't repeat that. The leading icon is vertically centered when a [subtitle] +/// is present, matching the surrounding settings styling. +class SettingsCheckboxTile extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final bool value; + final ValueChanged onChanged; + + const SettingsCheckboxTile({ + required this.icon, + required this.title, + required this.value, + required this.onChanged, + this.subtitle, + super.key, + }); + + @override + Widget build(BuildContext context) { + final leadingIcon = Icon(icon); + return ListTile( + leading: subtitle == null ? leadingIcon : CenteredLeading(leadingIcon), + title: Text(title), + subtitle: subtitle == null ? null : Text(subtitle!), + trailing: Checkbox( + value: value, + onChanged: (e) { + Haptics.selection(); + onChanged(e ?? false); + }, + ), + ); + } +}