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,16 +173,23 @@ 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) =>
// const AutoSleepTimerSettingsPage(),
pageBuilder: defaultPageBuilder( pageBuilder: defaultPageBuilder(
const AutoSleepTimerSettingsPage(), 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,
name: Routes.userManagement.name, name: Routes.userManagement.name,

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,12 +27,32 @@ 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'),
),
body: SettingsList(
sections: [ sections: [
// General 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);
},
),
],
),
// Appearance section // Appearance section
SettingsSection( SettingsSection(
margin: const EdgeInsetsDirectional.symmetric( margin: const EdgeInsetsDirectional.symmetric(
@ -272,7 +293,6 @@ class AppSettingsPage extends HookConsumerWidget {
], ],
), ),
], ],
),
); );
} }
} }

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,11 +15,8 @@ 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'),
),
body: SettingsList(
sections: [ sections: [
SettingsSection( SettingsSection(
margin: const EdgeInsetsDirectional.symmetric( margin: const EdgeInsetsDirectional.symmetric(
@ -55,22 +53,18 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
// navigate to the time picker // navigate to the time picker
final selected = await showTimePicker( final selected = await showTimePicker(
context: context, context: context,
initialTime: initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
); );
if (selected != null) { if (selected != null) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings appSettings.copyWith.playerSettings.sleepTimerSettings(
.sleepTimerSettings(
autoTurnOnTime: selected.toDuration(), autoTurnOnTime: selected.toDuration(),
), ),
); );
} }
}, },
value: Text( value: Text(
sleepTimerSettings.autoTurnOnTime sleepTimerSettings.autoTurnOnTime.toTimeOfDay().format(context),
.toTimeOfDay()
.format(context),
), ),
), ),
SettingsTile.navigation( SettingsTile.navigation(
@ -83,13 +77,11 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
// navigate to the time picker // navigate to the time picker
final selected = await showTimePicker( final selected = await showTimePicker(
context: context, context: context,
initialTime: initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
); );
if (selected != null) { if (selected != null) {
ref.read(appSettingsProvider.notifier).update( ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings appSettings.copyWith.playerSettings.sleepTimerSettings(
.sleepTimerSettings(
autoTurnOffTime: selected.toDuration(), autoTurnOffTime: selected.toDuration(),
), ),
); );
@ -104,7 +96,6 @@ class AutoSleepTimerSettingsPage extends HookConsumerWidget {
], ],
), ),
], ],
),
); );
} }
} }

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