feat: add notification settings page and update routing

This commit is contained in:
Dr-Blank 2024-09-25 02:53:14 -04:00
parent e89cda83c2
commit c0c5cf6f4d
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
7 changed files with 823 additions and 337 deletions

View file

@ -28,9 +28,14 @@ class Routes {
name: 'settings', name: 'settings',
); );
static const autoSleepTimerSettings = _SimpleRoute( static const autoSleepTimerSettings = _SimpleRoute(
pathName: 'autosleeptimer', pathName: 'autoSleepTimer',
name: 'autoSleepTimerSettings', name: 'autoSleepTimerSettings',
// parentRoute: settings, parentRoute: settings,
);
static const notificationSettings = _SimpleRoute(
pathName: 'notifications',
name: 'notificationSettings',
parentRoute: settings,
); );
// search and explore // search and explore

View file

@ -12,6 +12,7 @@ import 'package:vaani/features/you/view/you_page.dart';
import 'package:vaani/pages/home_page.dart'; import 'package:vaani/pages/home_page.dart';
import 'package:vaani/settings/view/app_settings_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/auto_sleep_timer_settings_page.dart';
import 'package:vaani/settings/view/notification_settings_page.dart';
import 'scaffold_with_nav_bar.dart'; import 'scaffold_with_nav_bar.dart';
import 'transitions/slide.dart'; import 'transitions/slide.dart';
@ -172,15 +173,22 @@ class MyAppRouter {
name: Routes.settings.name, name: Routes.settings.name,
// builder: (context, state) => const AppSettingsPage(), // builder: (context, state) => const AppSettingsPage(),
pageBuilder: defaultPageBuilder(const AppSettingsPage()), pageBuilder: defaultPageBuilder(const AppSettingsPage()),
), routes: [
GoRoute( GoRoute(
path: Routes.autoSleepTimerSettings.localPath, path: Routes.autoSleepTimerSettings.pathName,
name: Routes.autoSleepTimerSettings.name, name: Routes.autoSleepTimerSettings.name,
// builder: (context, state) => pageBuilder: defaultPageBuilder(
// const AutoSleepTimerSettingsPage(), const AutoSleepTimerSettingsPage(),
pageBuilder: defaultPageBuilder( ),
const AutoSleepTimerSettingsPage(), ),
), GoRoute(
path: Routes.notificationSettings.pathName,
name: Routes.notificationSettings.name,
pageBuilder: defaultPageBuilder(
const NotificationSettingsPage(),
),
),
],
), ),
GoRoute( GoRoute(
path: Routes.userManagement.localPath, path: Routes.userManagement.localPath,

View file

@ -1,5 +1,6 @@
// a freezed class to store the settings of the app // a freezed class to store the settings of the app
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_settings.freezed.dart'; part 'app_settings.freezed.dart';
@ -173,10 +174,14 @@ enum NotificationTitleType {
} }
enum NotificationMediaControl { enum NotificationMediaControl {
fastForward, fastForward(Icons.fast_forward),
rewind, rewind(Icons.fast_rewind),
speedToggle, speedToggle(Icons.speed),
stop, stop(Icons.stop),
skipToNextChapter, skipToNextChapter(Icons.skip_next),
skipToPreviousChapter, skipToPreviousChapter(Icons.skip_previous);
const NotificationMediaControl(this.icon);
final IconData icon;
} }

View file

@ -11,6 +11,7 @@ import 'package:vaani/api/server_provider.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart' as model; import 'package:vaani/settings/models/app_settings.dart' as model;
import 'package:vaani/settings/view/simple_settings_page.dart';
class AppSettingsPage extends HookConsumerWidget { class AppSettingsPage extends HookConsumerWidget {
const AppSettingsPage({ const AppSettingsPage({
@ -26,253 +27,272 @@ class AppSettingsPage extends HookConsumerWidget {
final serverURIController = useTextEditingController(); final serverURIController = useTextEditingController();
final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings;
return Scaffold( return SimpleSettingsPage(
appBar: AppBar( title: const Text('App Settings'),
title: const Text('App Settings'), sections: [
), // General section
body: SettingsList( SettingsSection(
sections: [ margin: const EdgeInsetsDirectional.symmetric(
// Appearance section horizontal: 16.0,
SettingsSection( vertical: 8.0,
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,
),
);
},
),
],
), ),
title: Text(
// Sleep Timer section 'General',
SettingsSection( style: Theme.of(context).textTheme.titleLarge,
margin: const EdgeInsetsDirectional.symmetric( ),
horizontal: 16.0, tiles: [
vertical: 8.0, 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.switchTile(
SettingsTile.navigation( initialValue:
// initialValue: sleepTimerSettings.autoTurnOnTimer, appSettings.themeSettings.useMaterialThemeOnItemPage,
title: const Text('Auto Turn On Timer'), title: const Text('Adaptive Theme on Item Page'),
description: const Text( description: const Text(
'Automatically turn on the sleep timer based on the time of day', 'get fancy with the colors on the item page at the cost of some performance',
), ),
leading: sleepTimerSettings.autoTurnOnTimer leading: appSettings.themeSettings.useMaterialThemeOnItemPage
? const Icon(Icons.timer) ? const Icon(Icons.auto_fix_high)
: const Icon(Icons.timer_off), : const Icon(Icons.auto_fix_off),
onPressed: (context) { onToggle: (value) {
// push the sleep timer settings page ref.read(appSettingsProvider.notifier).update(
context.pushNamed(Routes.autoSleepTimerSettings.name); appSettings.copyWith.themeSettings(
}, useMaterialThemeOnItemPage: value,
// 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, // 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<FormState>();
// 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?',
), ),
], actions: [
), TextButton(
), onPressed: () {
), Navigator.of(context).pop(false);
], },
), child: const Text('Cancel'),
// 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<FormState>();
// 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(
TextButton( onPressed: () {
onPressed: () { Navigator.of(context).pop(true);
Navigator.of(context).pop(); },
}, child: const Text('Reset'),
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?',
), ),
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 the user confirms the reset
if (res == true) { if (res == true) {
ref.read(appSettingsProvider.notifier).reset(); ref.read(appSettingsProvider.notifier).reset();
} }
}, },
), ),
], ],
), ),
], ],
),
); );
} }
} }

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/settings/app_settings_provider.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'; import 'package:vaani/shared/extensions/time_of_day.dart';
class AutoSleepTimerSettingsPage extends HookConsumerWidget { class AutoSleepTimerSettingsPage extends HookConsumerWidget {
@ -14,97 +15,87 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
final appSettings = ref.watch(appSettingsProvider); final appSettings = ref.watch(appSettingsProvider);
final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings;
return Scaffold( return SimpleSettingsPage(
appBar: AppBar( title: const Text('Auto Sleep Timer Settings'),
title: const Text('Auto Sleep Timer Settings'), sections: [
), SettingsSection(
body: SettingsList( margin: const EdgeInsetsDirectional.symmetric(
sections: [ horizontal: 16.0,
SettingsSection( vertical: 8.0,
margin: const EdgeInsetsDirectional.symmetric( ),
horizontal: 16.0, tiles: [
vertical: 8.0, 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: [ // auto turn on time settings, enabled only when autoTurnOnTimer is enabled
SettingsTile.switchTile( SettingsTile.navigation(
// initialValue: sleepTimerSettings.autoTurnOnTimer, enabled: sleepTimerSettings.autoTurnOnTimer,
title: const Text('Auto Turn On Timer'), title: const Text('Auto Turn On Time'),
description: const Text( description: const Text(
'Automatically turn on the sleep timer based on the time of day', 'Turn on the sleep timer at the specified time',
), ),
leading: sleepTimerSettings.autoTurnOnTimer onPressed: (context) async {
? const Icon(Icons.timer) // navigate to the time picker
: const Icon(Icons.timer_off), final selected = await showTimePicker(
onToggle: (value) { context: context,
initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
);
if (selected != null) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings.sleepTimerSettings( 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( SettingsTile.navigation(
enabled: sleepTimerSettings.autoTurnOnTimer, title: const Text('Auto Turn Off Time'),
title: const Text('Auto Turn On Time'), description: const Text(
description: const Text( 'Turn off the sleep timer at the specified time',
'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( enabled: sleepTimerSettings.autoTurnOnTimer,
title: const Text('Auto Turn Off Time'), onPressed: (context) async {
description: const Text( // navigate to the time picker
'Turn off the sleep timer at the specified time', final selected = await showTimePicker(
), context: context,
enabled: sleepTimerSettings.autoTurnOnTimer, initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
onPressed: (context) async { );
// navigate to the time picker if (selected != null) {
final selected = await showTimePicker( ref.read(appSettingsProvider.notifier).update(
context: context, appSettings.copyWith.playerSettings.sleepTimerSettings(
initialTime: autoTurnOffTime: selected.toDuration(),
sleepTimerSettings.autoTurnOffTime.toTimeOfDay(), ),
); );
if (selected != null) { }
ref.read(appSettingsProvider.notifier).update( },
appSettings.copyWith.playerSettings value: Text(
.sleepTimerSettings( sleepTimerSettings.autoTurnOffTime
autoTurnOffTime: selected.toDuration(), .toTimeOfDay()
), .format(context),
);
}
},
value: Text(
sleepTimerSettings.autoTurnOffTime
.toTimeOfDay()
.format(context),
),
), ),
], ),
), ],
], ),
), ],
); );
} }
} }

View file

@ -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<String>(
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<String>(
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<List<NotificationMediaControl>>(
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<NotificationMediaControl> 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<Duration>? onChanged;
final ValueChanged<Duration>? 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<String?> showNotificationTitlePicker(
BuildContext context, {
required String initialValue,
required String title,
}) async {
return showDialog<String>(
context: context,
builder: (context) {
return NotificationTitlePicker(initialValue: initialValue, title: title);
},
);
}

View file

@ -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<AbstractSettingsSection>? 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!,
),
),
],
),
),
],
),
);
}
}