From c0c5cf6f4d0c6dbb3beb38c80154c29ee31b569c Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Wed, 25 Sep 2024 02:53:14 -0400 Subject: [PATCH] feat: add notification settings page and update routing --- lib/router/constants.dart | 9 +- lib/router/router.dart | 26 +- lib/settings/models/app_settings.dart | 17 +- lib/settings/view/app_settings_page.dart | 494 +++++++++--------- .../view/auto_sleep_timer_settings_page.dart | 157 +++--- .../view/notification_settings_page.dart | 404 ++++++++++++++ lib/settings/view/simple_settings_page.dart | 53 ++ 7 files changed, 823 insertions(+), 337 deletions(-) create mode 100644 lib/settings/view/notification_settings_page.dart create mode 100644 lib/settings/view/simple_settings_page.dart diff --git a/lib/router/constants.dart b/lib/router/constants.dart index 5073e60..9dd15ad 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -28,9 +28,14 @@ class Routes { name: 'settings', ); static const autoSleepTimerSettings = _SimpleRoute( - pathName: 'autosleeptimer', + pathName: 'autoSleepTimer', name: 'autoSleepTimerSettings', - // parentRoute: settings, + parentRoute: settings, + ); + static const notificationSettings = _SimpleRoute( + pathName: 'notifications', + name: 'notificationSettings', + parentRoute: settings, ); // search and explore diff --git a/lib/router/router.dart b/lib/router/router.dart index b3085ee..8781e6f 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -12,6 +12,7 @@ import 'package:vaani/features/you/view/you_page.dart'; import 'package:vaani/pages/home_page.dart'; import 'package:vaani/settings/view/app_settings_page.dart'; import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart'; +import 'package:vaani/settings/view/notification_settings_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; @@ -172,15 +173,22 @@ class MyAppRouter { name: Routes.settings.name, // builder: (context, state) => const AppSettingsPage(), pageBuilder: defaultPageBuilder(const AppSettingsPage()), - ), - GoRoute( - path: Routes.autoSleepTimerSettings.localPath, - name: Routes.autoSleepTimerSettings.name, - // builder: (context, state) => - // const AutoSleepTimerSettingsPage(), - pageBuilder: defaultPageBuilder( - const AutoSleepTimerSettingsPage(), - ), + routes: [ + GoRoute( + path: Routes.autoSleepTimerSettings.pathName, + name: Routes.autoSleepTimerSettings.name, + pageBuilder: defaultPageBuilder( + const AutoSleepTimerSettingsPage(), + ), + ), + GoRoute( + path: Routes.notificationSettings.pathName, + name: Routes.notificationSettings.name, + pageBuilder: defaultPageBuilder( + const NotificationSettingsPage(), + ), + ), + ], ), GoRoute( path: Routes.userManagement.localPath, diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 9dc5eb8..cc4506b 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -1,5 +1,6 @@ // a freezed class to store the settings of the app +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'app_settings.freezed.dart'; @@ -173,10 +174,14 @@ enum NotificationTitleType { } enum NotificationMediaControl { - fastForward, - rewind, - speedToggle, - stop, - skipToNextChapter, - skipToPreviousChapter, + fastForward(Icons.fast_forward), + rewind(Icons.fast_rewind), + speedToggle(Icons.speed), + stop(Icons.stop), + skipToNextChapter(Icons.skip_next), + skipToPreviousChapter(Icons.skip_previous); + + const NotificationMediaControl(this.icon); + + final IconData icon; } diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart index 6cf2298..bca69fe 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -11,6 +11,7 @@ import 'package:vaani/api/server_provider.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/models/app_settings.dart' as model; +import 'package:vaani/settings/view/simple_settings_page.dart'; class AppSettingsPage extends HookConsumerWidget { const AppSettingsPage({ @@ -26,253 +27,272 @@ class AppSettingsPage extends HookConsumerWidget { final serverURIController = useTextEditingController(); final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; - return Scaffold( - appBar: AppBar( - title: const Text('App Settings'), - ), - body: SettingsList( - sections: [ - // Appearance section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - title: Text( - 'Appearance', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile.switchTile( - initialValue: appSettings.themeSettings.isDarkMode, - title: const Text('Dark Mode'), - description: const Text('we all know dark mode is better'), - leading: appSettings.themeSettings.isDarkMode - ? const Icon(Icons.dark_mode) - : const Icon(Icons.light_mode), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).toggleDarkMode(); - }, - ), - SettingsTile.switchTile( - initialValue: - appSettings.themeSettings.useMaterialThemeOnItemPage, - title: const Text('Adaptive Theme on Item Page'), - description: const Text( - 'get fancy with the colors on the item page at the cost of some performance', - ), - leading: appSettings.themeSettings.useMaterialThemeOnItemPage - ? const Icon(Icons.auto_fix_high) - : const Icon(Icons.auto_fix_off), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.themeSettings( - useMaterialThemeOnItemPage: value, - ), - ); - }, - ), - ], + return SimpleSettingsPage( + title: const Text('App Settings'), + sections: [ + // General section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - - // Sleep Timer section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, + title: Text( + 'General', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile( + title: const Text('Notification Media Player'), + leading: const Icon(Icons.play_lesson), + description: const Text( + 'Customize the media player in notifications', + ), + onPressed: (context) { + context.pushNamed(Routes.notificationSettings.name); + }, ), - title: Text( - 'Sleep Timer', - style: Theme.of(context).textTheme.titleLarge, + ], + ), + // Appearance section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Text( + 'Appearance', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile.switchTile( + initialValue: appSettings.themeSettings.isDarkMode, + title: const Text('Dark Mode'), + description: const Text('we all know dark mode is better'), + leading: appSettings.themeSettings.isDarkMode + ? const Icon(Icons.dark_mode) + : const Icon(Icons.light_mode), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).toggleDarkMode(); + }, ), - tiles: [ - SettingsTile.navigation( - // initialValue: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Timer'), - description: const Text( - 'Automatically turn on the sleep timer based on the time of day', - ), - leading: sleepTimerSettings.autoTurnOnTimer - ? const Icon(Icons.timer) - : const Icon(Icons.timer_off), - onPressed: (context) { - // push the sleep timer settings page - context.pushNamed(Routes.autoSleepTimerSettings.name); - }, - // a switch to enable or disable the auto turn off time - trailing: IntrinsicHeight( - child: Row( - children: [ - VerticalDivider( - color: Theme.of(context).dividerColor.withOpacity(0.5), - indent: 8.0, - endIndent: 8.0, - // width: 8.0, - // thickness: 2.0, - // height: 24.0, + SettingsTile.switchTile( + initialValue: + appSettings.themeSettings.useMaterialThemeOnItemPage, + title: const Text('Adaptive Theme on Item Page'), + description: const Text( + 'get fancy with the colors on the item page at the cost of some performance', + ), + leading: appSettings.themeSettings.useMaterialThemeOnItemPage + ? const Icon(Icons.auto_fix_high) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.themeSettings( + useMaterialThemeOnItemPage: value, ), - Switch( - value: sleepTimerSettings.autoTurnOnTimer, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOnTimer: value, + ); + }, + ), + ], + ), + + // Sleep Timer section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Text( + 'Sleep Timer', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile.navigation( + // initialValue: sleepTimerSettings.autoTurnOnTimer, + title: const Text('Auto Turn On Timer'), + description: const Text( + 'Automatically turn on the sleep timer based on the time of day', + ), + leading: sleepTimerSettings.autoTurnOnTimer + ? const Icon(Icons.timer) + : const Icon(Icons.timer_off), + onPressed: (context) { + // push the sleep timer settings page + context.pushNamed(Routes.autoSleepTimerSettings.name); + }, + // a switch to enable or disable the auto turn off time + trailing: IntrinsicHeight( + child: Row( + children: [ + VerticalDivider( + color: Theme.of(context).dividerColor.withOpacity(0.5), + indent: 8.0, + endIndent: 8.0, + // width: 8.0, + // thickness: 2.0, + // height: 24.0, + ), + Switch( + value: sleepTimerSettings.autoTurnOnTimer, + onChanged: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.playerSettings + .sleepTimerSettings( + autoTurnOnTimer: value, + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + + // Backup and Restore section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Text( + 'Backup and Restore', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile( + title: const Text('Copy to Clipboard'), + leading: const Icon(Icons.copy), + description: const Text( + 'Copy the app settings to the clipboard', + ), + onPressed: (context) async { + // copy to clipboard + await Clipboard.setData( + ClipboardData( + text: jsonEncode(appSettings.toJson()), + ), + ); + // show toast + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings copied to clipboard'), + ), + ); + }, + ), + SettingsTile( + title: const Text('Restore'), + leading: const Icon(Icons.restore), + description: const Text( + 'Restore the app settings from the backup', + ), + onPressed: (context) { + final formKey = GlobalKey(); + // show a dialog to get the backup + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Restore Backup'), + content: Form( + key: formKey, + child: TextFormField( + controller: serverURIController, + decoration: const InputDecoration( + labelText: 'Backup', + hintText: 'Paste the backup here', + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please paste the backup here'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + final backup = serverURIController.text; + final newSettings = model.AppSettings.fromJson( + // decode the backup as json + jsonDecode(backup), + ); + ref + .read(appSettingsProvider.notifier) + .update(newSettings); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings restored'), ), ); - }, + // clear the backup + serverURIController.clear(); + } + }, + child: const Text('Restore'), + ), + ], + ); + }, + ); + }, + ), + + // a button to reset the app settings + SettingsTile( + title: const Text('Reset App Settings'), + leading: const Icon(Icons.settings_backup_restore), + description: const Text( + 'Reset the app settings to the default values', + ), + onPressed: (context) async { + // confirm the reset + final res = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Reset App Settings'), + content: const Text( + 'Are you sure you want to reset the app settings?', ), - ], - ), - ), - ), - ], - ), - - // Backup and Restore section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - title: Text( - 'Backup and Restore', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile( - title: const Text('Copy to Clipboard'), - leading: const Icon(Icons.copy), - description: const Text( - 'Copy the app settings to the clipboard', - ), - onPressed: (context) async { - // copy to clipboard - await Clipboard.setData( - ClipboardData( - text: jsonEncode(appSettings.toJson()), - ), - ); - // show toast - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings copied to clipboard'), - ), - ); - }, - ), - SettingsTile( - title: const Text('Restore'), - leading: const Icon(Icons.restore), - description: const Text( - 'Restore the app settings from the backup', - ), - onPressed: (context) { - final formKey = GlobalKey(); - // show a dialog to get the backup - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Restore Backup'), - content: Form( - key: formKey, - child: TextFormField( - controller: serverURIController, - decoration: const InputDecoration( - labelText: 'Backup', - hintText: 'Paste the backup here', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please paste the backup here'; - } - return null; - }, - ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - if (formKey.currentState!.validate()) { - final backup = serverURIController.text; - final newSettings = model.AppSettings.fromJson( - // decode the backup as json - jsonDecode(backup), - ); - ref - .read(appSettingsProvider.notifier) - .update(newSettings); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings restored'), - ), - ); - // clear the backup - serverURIController.clear(); - } - }, - child: const Text('Restore'), - ), - ], - ); - }, - ); - }, - ), - - // a button to reset the app settings - SettingsTile( - title: const Text('Reset App Settings'), - leading: const Icon(Icons.settings_backup_restore), - description: const Text( - 'Reset the app settings to the default values', - ), - onPressed: (context) async { - // confirm the reset - final res = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Reset App Settings'), - content: const Text( - 'Are you sure you want to reset the app settings?', + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Reset'), ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: const Text('Reset'), - ), - ], - ); - }, - ); + ], + ); + }, + ); - // if the user confirms the reset - if (res == true) { - ref.read(appSettingsProvider.notifier).reset(); - } - }, - ), - ], - ), - ], - ), + // if the user confirms the reset + if (res == true) { + ref.read(appSettingsProvider.notifier).reset(); + } + }, + ), + ], + ), + ], ); } } diff --git a/lib/settings/view/auto_sleep_timer_settings_page.dart b/lib/settings/view/auto_sleep_timer_settings_page.dart index 2383ba1..29524a9 100644 --- a/lib/settings/view/auto_sleep_timer_settings_page.dart +++ b/lib/settings/view/auto_sleep_timer_settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/time_of_day.dart'; class AutoSleepTimerSettingsPage extends HookConsumerWidget { @@ -14,97 +15,87 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget { final appSettings = ref.watch(appSettingsProvider); final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; - return Scaffold( - appBar: AppBar( - title: const Text('Auto Sleep Timer Settings'), - ), - body: SettingsList( - sections: [ - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, + return SimpleSettingsPage( + title: const Text('Auto Sleep Timer Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + SettingsTile.switchTile( + // initialValue: sleepTimerSettings.autoTurnOnTimer, + title: const Text('Auto Turn On Timer'), + description: const Text( + 'Automatically turn on the sleep timer based on the time of day', + ), + leading: sleepTimerSettings.autoTurnOnTimer + ? const Icon(Icons.timer) + : const Icon(Icons.timer_off), + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.playerSettings.sleepTimerSettings( + autoTurnOnTimer: value, + ), + ); + }, + initialValue: sleepTimerSettings.autoTurnOnTimer, ), - tiles: [ - SettingsTile.switchTile( - // initialValue: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Timer'), - description: const Text( - 'Automatically turn on the sleep timer based on the time of day', - ), - leading: sleepTimerSettings.autoTurnOnTimer - ? const Icon(Icons.timer) - : const Icon(Icons.timer_off), - onToggle: (value) { + // auto turn on time settings, enabled only when autoTurnOnTimer is enabled + SettingsTile.navigation( + enabled: sleepTimerSettings.autoTurnOnTimer, + title: const Text('Auto Turn On Time'), + description: const Text( + 'Turn on the sleep timer at the specified time', + ), + onPressed: (context) async { + // navigate to the time picker + final selected = await showTimePicker( + context: context, + initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(), + ); + if (selected != null) { ref.read(appSettingsProvider.notifier).update( appSettings.copyWith.playerSettings.sleepTimerSettings( - autoTurnOnTimer: value, + autoTurnOnTime: selected.toDuration(), ), ); - }, - initialValue: sleepTimerSettings.autoTurnOnTimer, + } + }, + value: Text( + sleepTimerSettings.autoTurnOnTime.toTimeOfDay().format(context), ), - // auto turn on time settings, enabled only when autoTurnOnTimer is enabled - SettingsTile.navigation( - enabled: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Time'), - description: const Text( - 'Turn on the sleep timer at the specified time', - ), - onPressed: (context) async { - // navigate to the time picker - final selected = await showTimePicker( - context: context, - initialTime: - sleepTimerSettings.autoTurnOnTime.toTimeOfDay(), - ); - if (selected != null) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOnTime: selected.toDuration(), - ), - ); - } - }, - value: Text( - sleepTimerSettings.autoTurnOnTime - .toTimeOfDay() - .format(context), - ), + ), + SettingsTile.navigation( + title: const Text('Auto Turn Off Time'), + description: const Text( + 'Turn off the sleep timer at the specified time', ), - SettingsTile.navigation( - title: const Text('Auto Turn Off Time'), - description: const Text( - 'Turn off the sleep timer at the specified time', - ), - enabled: sleepTimerSettings.autoTurnOnTimer, - onPressed: (context) async { - // navigate to the time picker - final selected = await showTimePicker( - context: context, - initialTime: - sleepTimerSettings.autoTurnOffTime.toTimeOfDay(), - ); - if (selected != null) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOffTime: selected.toDuration(), - ), - ); - } - }, - value: Text( - sleepTimerSettings.autoTurnOffTime - .toTimeOfDay() - .format(context), - ), + enabled: sleepTimerSettings.autoTurnOnTimer, + onPressed: (context) async { + // navigate to the time picker + final selected = await showTimePicker( + context: context, + initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(), + ); + if (selected != null) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.playerSettings.sleepTimerSettings( + autoTurnOffTime: selected.toDuration(), + ), + ); + } + }, + value: Text( + sleepTimerSettings.autoTurnOffTime + .toTimeOfDay() + .format(context), ), - ], - ), - ], - ), + ), + ], + ), + ], ); } } diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart new file mode 100644 index 0000000..df1c5e5 --- /dev/null +++ b/lib/settings/view/notification_settings_page.dart @@ -0,0 +1,404 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; + +class NotificationSettingsPage extends HookConsumerWidget { + const NotificationSettingsPage({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final notificationSettings = appSettings.notificationSettings; + + return SimpleSettingsPage( + title: const Text('Notification Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.only( + start: 16.0, + end: 16.0, + top: 8.0, + bottom: 8.0, + ), + tiles: [ + // set the primary and secondary titles + SettingsTile( + title: const Text('Primary Title'), + description: Text.rich( + TextSpan( + text: 'The title of the notification\n', + children: [ + TextSpan( + text: notificationSettings.primaryTitle, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + leading: const Icon(Icons.title), + onPressed: (context) async { + // show the notification title picker + final selectedTitle = await showDialog( + context: context, + builder: (context) { + return NotificationTitlePicker( + initialValue: notificationSettings.primaryTitle, + title: 'Primary Title', + ); + }, + ); + if (selectedTitle != null) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + primaryTitle: selectedTitle, + ), + ); + } + }, + ), + + SettingsTile( + title: const Text('Secondary Title'), + description: Text.rich( + TextSpan( + text: 'The subtitle of the notification\n', + children: [ + TextSpan( + text: notificationSettings.secondaryTitle, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + leading: const Icon(Icons.title), + onPressed: (context) async { + // show the notification title picker + final selectedTitle = await showDialog( + context: context, + builder: (context) { + return NotificationTitlePicker( + initialValue: notificationSettings.secondaryTitle, + title: 'Secondary Title', + ); + }, + ); + if (selectedTitle != null) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + secondaryTitle: selectedTitle, + ), + ); + } + }, + ), + + // set forward and backward intervals + SettingsTile( + title: const Text('Forward Interval'), + description: Row( + children: [ + Text( + '${notificationSettings.fastForwardInterval.inSeconds} seconds', + ), + Expanded( + child: TimeIntervalSlider( + defaultValue: notificationSettings.fastForwardInterval, + onChangedEnd: (interval) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + fastForwardInterval: interval, + ), + ); + }, + ), + ), + ], + ), + leading: const Icon(Icons.fast_forward), + ), + SettingsTile( + title: const Text('Backward Interval'), + description: Row( + children: [ + Text( + '${notificationSettings.rewindInterval.inSeconds} seconds', + ), + Expanded( + child: TimeIntervalSlider( + defaultValue: notificationSettings.rewindInterval, + onChangedEnd: (interval) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + rewindInterval: interval, + ), + ); + }, + ), + ), + ], + ), + leading: const Icon(Icons.fast_rewind), + ), + // set the media controls + SettingsTile( + title: const Text('Media Controls'), + leading: const Icon(Icons.control_camera), + // description: const Text('Select the media controls to display'), + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Select the media controls to display'), + Wrap( + spacing: 8.0, + children: notificationSettings.mediaControls + .map( + (control) => Icon(control.icon), + ) + .toList(), + ), + ], + ), + onPressed: (context) async { + final selectedControls = + await showDialog>( + context: context, + builder: (context) { + return MediaControlsPicker( + selectedControls: notificationSettings.mediaControls, + ); + }, + ); + if (selectedControls != null) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + mediaControls: selectedControls, + ), + ); + } + }, + ), + + // set the progress bar to show chapter progress + SettingsTile.switchTile( + title: const Text('Show Chapter Progress'), + leading: const Icon(Icons.book), + description: + const Text('instead of the overall progress of the book'), + initialValue: notificationSettings.progressBarIsChapterProgress, + onToggle: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith.notificationSettings( + progressBarIsChapterProgress: value, + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +class MediaControlsPicker extends HookConsumerWidget { + const MediaControlsPicker({ + super.key, + required this.selectedControls, + }); + + final List selectedControls; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedMediaControls = useState(selectedControls); + return AlertDialog( + title: const Text('Media Controls'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(selectedMediaControls.value); + }, + child: const Text('OK'), + ), + ], + // a list of chips to easily select the media controls to display + // with icons and labels + content: Wrap( + spacing: 8.0, + children: NotificationMediaControl.values + .map( + (control) => ChoiceChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(control.icon), + const SizedBox(width: 4.0), + Text(control.name), + ], + ), + selected: selectedMediaControls.value.contains(control), + onSelected: (selected) { + if (selected) { + selectedMediaControls.value = [ + ...selectedMediaControls.value, + control, + ]; + } else { + selectedMediaControls.value = [ + ...selectedMediaControls.value.where((c) => c != control), + ]; + } + }, + ), + ) + .toList(), + ), + ); + } +} + +class TimeIntervalSlider extends HookConsumerWidget { + const TimeIntervalSlider({ + super.key, + this.title, + required this.defaultValue, + this.onChanged, + this.onChangedEnd, + this.min = const Duration(seconds: 5), + this.max = const Duration(seconds: 120), + this.step = const Duration(seconds: 5), + }); + + final Widget? title; + final Duration defaultValue; + final ValueChanged? onChanged; + final ValueChanged? onChangedEnd; + final Duration min; + final Duration max; + final Duration step; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedInterval = useState(defaultValue); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + title ?? const SizedBox.shrink(), + if (title != null) const SizedBox(height: 8.0), + Slider( + value: selectedInterval.value.inSeconds.toDouble(), + min: min.inSeconds.toDouble(), + max: max.inSeconds.toDouble(), + divisions: ((max.inSeconds - min.inSeconds) ~/ step.inSeconds), + label: '${selectedInterval.value.inSeconds} seconds', + onChanged: (value) { + selectedInterval.value = Duration(seconds: value.toInt()); + onChanged?.call(selectedInterval.value); + }, + onChangeEnd: (value) { + onChangedEnd?.call(selectedInterval.value); + }, + ), + ], + ); + } +} + +class NotificationTitlePicker extends HookConsumerWidget { + const NotificationTitlePicker({ + super.key, + required this.initialValue, + required this.title, + }); + + final String initialValue; + final String title; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedTitle = useState(initialValue); + final controller = useTextEditingController(text: initialValue); + return AlertDialog( + title: Text(title), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(selectedTitle.value); + }, + child: const Text('OK'), + ), + ], + // a list of chips to easily insert available fields into the text field + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + autofocus: true, + controller: controller, + onChanged: (value) { + selectedTitle.value = value; + }, + decoration: InputDecoration( + helper: const Text('Select a field below to insert it'), + suffix: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + selectedTitle.value = ''; + }, + ), + ), + ), + const SizedBox(height: 8.0), + Wrap( + spacing: 8.0, + children: NotificationTitleType.values + .map( + (type) => ActionChip( + label: Text(type.stringValue), + onPressed: () { + final text = controller.text; + final newText = '$text\$${type.stringValue}'; + controller.text = newText; + selectedTitle.value = newText; + }, + ), + ) + .toList(), + ), + ], + ), + ); + } +} + +Future showNotificationTitlePicker( + BuildContext context, { + required String initialValue, + required String title, +}) async { + return showDialog( + context: context, + builder: (context) { + return NotificationTitlePicker(initialValue: initialValue, title: title); + }, + ); +} diff --git a/lib/settings/view/simple_settings_page.dart b/lib/settings/view/simple_settings_page.dart new file mode 100644 index 0000000..586e986 --- /dev/null +++ b/lib/settings/view/simple_settings_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SimpleSettingsPage extends HookConsumerWidget { + const SimpleSettingsPage({ + super.key, + this.title, + this.sections, + }); + + final Widget? title; + final List? sections; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + // appBar: AppBar( + // title: title, + // ), + // body: body, + // an app bar which is bigger than the default app bar but on scroll shrinks to the default app bar with the title being animated + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 200.0, + floating: false, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: title, + // background: Theme.of(context).primaryColor, + ), + ), + if (sections != null) + SliverList( + delegate: SliverChildListDelegate( + [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(20)), + child: SettingsList( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + sections: sections!, + ), + ), + ], + ), + ), + ], + ), + ); + } +}