feat: add player settings page and enhance settings UI (#33)

This commit is contained in:
Dr.Blank 2024-09-26 04:30:51 -04:00 committed by GitHub
parent 8049a660e6
commit 150e5c9025
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 761 additions and 157 deletions

View file

@ -166,7 +166,8 @@ class PlaybackReporter {
return _session!;
}
if (player.book == null) {
throw NoAudiobookPlayingError();
_logger.warning('No audiobook playing to start session');
return null;
}
_session = await authenticatedApi.items.play(
libraryItemId: player.book!.libraryItemId,
@ -204,8 +205,11 @@ class PlaybackReporter {
}
try {
_session ??= await startSession();
} on NoAudiobookPlayingError {
_logger.warning('No audiobook playing to sync position');
} on Error catch (e) {
_logger.warning('Error starting session: $e');
}
if (_session == null) {
_logger.warning('No session to sync position');
return;
}
final currentPosition = player.positionInBook;

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -16,7 +18,8 @@ class SpeedSelector extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final speeds = appSettings.playerSettings.speedOptions;
final playerSettings = appSettings.playerSettings;
final speeds = playerSettings.speedOptions;
final currentSpeed = ref.watch(audiobookPlayerProvider).speed;
final speedState = useState(currentSpeed);
@ -30,10 +33,16 @@ class SpeedSelector extends HookConsumerWidget {
);
// the speed options
const minSpeed = 0.1;
const maxSpeed = 4.0;
const speedIncrement = 0.05;
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil();
final minSpeed = min(
speeds.reduce((minSpeedSoFar, element) => min(minSpeedSoFar, element)),
playerSettings.minSpeed,
);
final maxSpeed = max(
speeds.reduce((maxSpeedSoFar, element) => max(maxSpeedSoFar, element)),
playerSettings.maxSpeed,
);
final speedIncrement = playerSettings.speedIncrement;
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil() + 1;
final availableSpeedsList = List.generate(
availableSpeeds,
(index) {
@ -52,8 +61,8 @@ class SpeedSelector extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: Padding(
// the title
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
@ -62,112 +71,42 @@ class SpeedSelector extends HookConsumerWidget {
),
),
),
),
// the speed selector
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// a minus button to decrease the speed
IconButton.filledTonal(
icon: const Icon(Icons.remove),
onPressed: () {
// animate to index - 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index > 0) {
scrollController.animateToItem(
index - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
Expanded(
child: ListWheelScrollViewX(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: SpeedWheel(
availableSpeedsList: availableSpeedsList,
speedState: speedState,
scrollController: scrollController,
itemExtent: itemExtent,
diameterRatio: 1.5, squeeze: 1.2,
// useMagnifier: true,
// magnification: 1.5,
physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList
.map(
(speed) => Column(
children: [
// a vertical line
Container(
height: itemExtent * 2,
// thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05
width: speed % 0.5 == 0
? 3
: speed % 0.25 == 0
? 2
: 0.5,
color:
Theme.of(context).colorScheme.onSurface,
),
// the speed text but only at .5 increments of speed
if (speed % 0.25 == 0)
Text(
speed.toString(),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
),
],
),
)
.toList(),
onSelectedItemChanged: (index) {
speedState.value = availableSpeedsList[index];
// onSpeedSelected(availableSpeedsList[index]);
// call after 500ms to avoid the scrollview from scrolling to the selected speed
// Future.delayed(
// const Duration(milliseconds: 100),
// () => onSpeedSelected(availableSpeedsList[index]),
// );
},
),
),
// a plus button to increase the speed
IconButton.filledTonal(
icon: const Icon(Icons.add),
onPressed: () {
// animate to index + 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index < availableSpeedsList.length - 1) {
scrollController.animateToItem(
index + 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// the speed buttons
Wrap(
spacing: 8.0,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: speeds
.map(
(speed) => Flexible(
// the text button should be highlighted if the speed is selected
child: TextButton(
(speed) => TextButton(
style: speed == speedState.value
? TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
)
: null,
// border if not selected
: TextButton.styleFrom(
side: BorderSide(
color: Theme.of(context)
.colorScheme
.primaryContainer,
),
),
onPressed: () async {
// animate the wheel to the selected speed
var index = availableSpeedsList.indexOf(speed);
@ -191,15 +130,141 @@ class SpeedSelector extends HookConsumerWidget {
},
child: Text('$speed'),
),
),
)
.toList(),
),
const SizedBox(
height: 8,
),
],
),
);
}
}
class SpeedWheel extends StatelessWidget {
const SpeedWheel({
super.key,
required this.availableSpeedsList,
required this.speedState,
required this.scrollController,
required this.itemExtent,
this.showIncrementButtons = true,
});
final List<double> availableSpeedsList;
final ValueNotifier<double> speedState;
final FixedExtentScrollController scrollController;
final double itemExtent;
final bool showIncrementButtons;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// a minus button to decrease the speed
if (showIncrementButtons)
IconButton.filledTonal(
icon: const Icon(Icons.remove),
onPressed: () {
// animate to index - 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index > 0) {
scrollController.animateToItem(
index - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
// the speed selector wheel
Expanded(
child: ListWheelScrollViewX(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemExtent: itemExtent,
diameterRatio: 1.5, squeeze: 1.2,
// useMagnifier: true,
// magnification: 1.5,
physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList
.map(
(speed) => Expanded(
child: SpeedLine(itemExtent: itemExtent, speed: speed),
),
)
.toList(),
onSelectedItemChanged: (index) {
speedState.value = availableSpeedsList[index];
},
),
),
if (showIncrementButtons)
// a plus button to increase the speed
IconButton.filledTonal(
icon: const Icon(Icons.add),
onPressed: () {
// animate to index + 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index < availableSpeedsList.length - 1) {
scrollController.animateToItem(
index + 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
],
);
}
}
class SpeedLine extends StatelessWidget {
const SpeedLine({
super.key,
required this.itemExtent,
required this.speed,
});
final double itemExtent;
final double speed;
@override
Widget build(BuildContext context) {
return Column(
children: [
// a vertical line
Container(
height: itemExtent * 2,
// thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05
width: speed % 0.5 == 0
? 3
: speed % 0.25 == 0
? 2
: 0.5,
color: Theme.of(context).colorScheme.onSurface,
),
// the speed text but only at .5 increments of speed
if (speed % 0.25 == 0)
Expanded(
child: Text.rich(
TextSpan(
text: speed.floor().toString(),
children: [
TextSpan(
text: '.${speed.toStringAsFixed(2).split('.').last}',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelSmall?.fontSize,
),
),
],
),
),
),
],
);
}
}

View file

@ -37,6 +37,11 @@ class Routes {
name: 'notificationSettings',
parentRoute: settings,
);
static const playerSettings = _SimpleRoute(
pathName: 'player',
name: 'playerSettings',
parentRoute: settings,
);
// search and explore
static const search = _SimpleRoute(

View file

@ -13,6 +13,7 @@ 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 'package:vaani/settings/view/player_settings_page.dart';
import 'scaffold_with_nav_bar.dart';
import 'transitions/slide.dart';
@ -188,6 +189,12 @@ class MyAppRouter {
const NotificationSettingsPage(),
),
),
GoRoute(
path: Routes.playerSettings.pathName,
name: Routes.playerSettings.name,
pageBuilder:
defaultPageBuilder(const PlayerSettingsPage()),
),
],
),
GoRoute(

View file

@ -44,6 +44,9 @@ class PlayerSettings with _$PlayerSettings {
@Default(1) double preferredDefaultVolume,
@Default(1) double preferredDefaultSpeed,
@Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List<double> speedOptions,
@Default(0.05) double speedIncrement,
@Default(0.1) double minSpeed,
@Default(4) double maxSpeed,
@Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings,
@Default(Duration(seconds: 10)) Duration minimumPositionForReporting,
@Default(Duration(seconds: 10)) Duration playbackReportInterval,

View file

@ -512,6 +512,9 @@ mixin _$PlayerSettings {
double get preferredDefaultVolume => throw _privateConstructorUsedError;
double get preferredDefaultSpeed => throw _privateConstructorUsedError;
List<double> get speedOptions => throw _privateConstructorUsedError;
double get speedIncrement => throw _privateConstructorUsedError;
double get minSpeed => throw _privateConstructorUsedError;
double get maxSpeed => throw _privateConstructorUsedError;
SleepTimerSettings get sleepTimerSettings =>
throw _privateConstructorUsedError;
Duration get minimumPositionForReporting =>
@ -542,6 +545,9 @@ abstract class $PlayerSettingsCopyWith<$Res> {
double preferredDefaultVolume,
double preferredDefaultSpeed,
List<double> speedOptions,
double speedIncrement,
double minSpeed,
double maxSpeed,
SleepTimerSettings sleepTimerSettings,
Duration minimumPositionForReporting,
Duration playbackReportInterval,
@ -573,6 +579,9 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
Object? preferredDefaultVolume = null,
Object? preferredDefaultSpeed = null,
Object? speedOptions = null,
Object? speedIncrement = null,
Object? minSpeed = null,
Object? maxSpeed = null,
Object? sleepTimerSettings = null,
Object? minimumPositionForReporting = null,
Object? playbackReportInterval = null,
@ -600,6 +609,18 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
? _value.speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>,
speedIncrement: null == speedIncrement
? _value.speedIncrement
: speedIncrement // ignore: cast_nullable_to_non_nullable
as double,
minSpeed: null == minSpeed
? _value.minSpeed
: minSpeed // ignore: cast_nullable_to_non_nullable
as double,
maxSpeed: null == maxSpeed
? _value.maxSpeed
: maxSpeed // ignore: cast_nullable_to_non_nullable
as double,
sleepTimerSettings: null == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
@ -671,6 +692,9 @@ abstract class _$$PlayerSettingsImplCopyWith<$Res>
double preferredDefaultVolume,
double preferredDefaultSpeed,
List<double> speedOptions,
double speedIncrement,
double minSpeed,
double maxSpeed,
SleepTimerSettings sleepTimerSettings,
Duration minimumPositionForReporting,
Duration playbackReportInterval,
@ -703,6 +727,9 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
Object? preferredDefaultVolume = null,
Object? preferredDefaultSpeed = null,
Object? speedOptions = null,
Object? speedIncrement = null,
Object? minSpeed = null,
Object? maxSpeed = null,
Object? sleepTimerSettings = null,
Object? minimumPositionForReporting = null,
Object? playbackReportInterval = null,
@ -730,6 +757,18 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
? _value._speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>,
speedIncrement: null == speedIncrement
? _value.speedIncrement
: speedIncrement // ignore: cast_nullable_to_non_nullable
as double,
minSpeed: null == minSpeed
? _value.minSpeed
: minSpeed // ignore: cast_nullable_to_non_nullable
as double,
maxSpeed: null == maxSpeed
? _value.maxSpeed
: maxSpeed // ignore: cast_nullable_to_non_nullable
as double,
sleepTimerSettings: null == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
@ -763,6 +802,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
this.preferredDefaultVolume = 1,
this.preferredDefaultSpeed = 1,
final List<double> speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2],
this.speedIncrement = 0.05,
this.minSpeed = 0.1,
this.maxSpeed = 4,
this.sleepTimerSettings = const SleepTimerSettings(),
this.minimumPositionForReporting = const Duration(seconds: 10),
this.playbackReportInterval = const Duration(seconds: 10),
@ -794,6 +836,15 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
return EqualUnmodifiableListView(_speedOptions);
}
@override
@JsonKey()
final double speedIncrement;
@override
@JsonKey()
final double minSpeed;
@override
@JsonKey()
final double maxSpeed;
@override
@JsonKey()
final SleepTimerSettings sleepTimerSettings;
@ -812,7 +863,7 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
@override
String toString() {
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, minimumPositionForReporting: $minimumPositionForReporting, playbackReportInterval: $playbackReportInterval, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft, configurePlayerForEveryBook: $configurePlayerForEveryBook)';
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, speedIncrement: $speedIncrement, minSpeed: $minSpeed, maxSpeed: $maxSpeed, sleepTimerSettings: $sleepTimerSettings, minimumPositionForReporting: $minimumPositionForReporting, playbackReportInterval: $playbackReportInterval, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft, configurePlayerForEveryBook: $configurePlayerForEveryBook)';
}
@override
@ -830,6 +881,12 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
other.preferredDefaultSpeed == preferredDefaultSpeed) &&
const DeepCollectionEquality()
.equals(other._speedOptions, _speedOptions) &&
(identical(other.speedIncrement, speedIncrement) ||
other.speedIncrement == speedIncrement) &&
(identical(other.minSpeed, minSpeed) ||
other.minSpeed == minSpeed) &&
(identical(other.maxSpeed, maxSpeed) ||
other.maxSpeed == maxSpeed) &&
(identical(other.sleepTimerSettings, sleepTimerSettings) ||
other.sleepTimerSettings == sleepTimerSettings) &&
(identical(other.minimumPositionForReporting,
@ -856,6 +913,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
preferredDefaultVolume,
preferredDefaultSpeed,
const DeepCollectionEquality().hash(_speedOptions),
speedIncrement,
minSpeed,
maxSpeed,
sleepTimerSettings,
minimumPositionForReporting,
playbackReportInterval,
@ -886,6 +946,9 @@ abstract class _PlayerSettings implements PlayerSettings {
final double preferredDefaultVolume,
final double preferredDefaultSpeed,
final List<double> speedOptions,
final double speedIncrement,
final double minSpeed,
final double maxSpeed,
final SleepTimerSettings sleepTimerSettings,
final Duration minimumPositionForReporting,
final Duration playbackReportInterval,
@ -906,6 +969,12 @@ abstract class _PlayerSettings implements PlayerSettings {
@override
List<double> get speedOptions;
@override
double get speedIncrement;
@override
double get minSpeed;
@override
double get maxSpeed;
@override
SleepTimerSettings get sleepTimerSettings;
@override
Duration get minimumPositionForReporting;

View file

@ -69,6 +69,9 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
?.map((e) => (e as num).toDouble())
.toList() ??
const [0.75, 1, 1.25, 1.5, 1.75, 2],
speedIncrement: (json['speedIncrement'] as num?)?.toDouble() ?? 0.05,
minSpeed: (json['minSpeed'] as num?)?.toDouble() ?? 0.1,
maxSpeed: (json['maxSpeed'] as num?)?.toDouble() ?? 4,
sleepTimerSettings: json['sleepTimerSettings'] == null
? const SleepTimerSettings()
: SleepTimerSettings.fromJson(
@ -98,6 +101,9 @@ Map<String, dynamic> _$$PlayerSettingsImplToJson(
'preferredDefaultVolume': instance.preferredDefaultVolume,
'preferredDefaultSpeed': instance.preferredDefaultSpeed,
'speedOptions': instance.speedOptions,
'speedIncrement': instance.speedIncrement,
'minSpeed': instance.minSpeed,
'maxSpeed': instance.maxSpeed,
'sleepTimerSettings': instance.sleepTimerSettings,
'minimumPositionForReporting':
instance.minimumPositionForReporting.inMicroseconds,

View file

@ -51,6 +51,16 @@ class AppSettingsPage extends HookConsumerWidget {
context.pushNamed(Routes.notificationSettings.name);
},
),
SettingsTile(
title: const Text('Player Settings'),
leading: const Icon(Icons.play_arrow),
description: const Text(
'Customize the player settings',
),
onPressed: (context) {
context.pushNamed(Routes.playerSettings.name);
},
),
],
),
// Appearance section

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class OkButton<T> extends StatelessWidget {
const OkButton({
super.key,
this.onPressed,
});
final void Function()? onPressed;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: const Text('OK'),
);
}
}
class CancelButton extends StatelessWidget {
const CancelButton({
super.key,
});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
);
}
}

View file

@ -4,6 +4,7 @@ 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/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart';
class NotificationSettingsPage extends HookConsumerWidget {
@ -220,17 +221,11 @@ class MediaControlsPicker extends HookConsumerWidget {
return AlertDialog(
title: const Text('Media Controls'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(selectedMediaControls.value);
},
child: const Text('OK'),
}
),
],
// a list of chips to easily select the media controls to display
@ -333,17 +328,11 @@ class NotificationTitlePicker extends HookConsumerWidget {
return AlertDialog(
title: Text(title),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(selectedTitle.value);
},
child: const Text('OK'),
}
),
],
// a list of chips to easily insert available fields into the text field

View file

@ -0,0 +1,410 @@
import 'package:duration_picker/duration_picker.dart';
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/view/buttons.dart';
import 'package:vaani/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
class PlayerSettingsPage extends HookConsumerWidget {
const PlayerSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final playerSettings = appSettings.playerSettings;
return SimpleSettingsPage(
title: const Text('Player Settings'),
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
tiles: [
// preferred settings for every book
SettingsTile.switchTile(
title: const Text('Remember Player Settings for Every Book'),
leading: const Icon(Icons.settings_applications),
description: const Text(
'Settings like speed, loudness, etc. will be remembered for every book',
),
initialValue: playerSettings.configurePlayerForEveryBook,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
configurePlayerForEveryBook: value,
),
);
},
),
// preferred default speed
SettingsTile(
title: const Text('Default Speed'),
description: Text('${playerSettings.preferredDefaultSpeed}x'),
leading: const Icon(Icons.speed),
onPressed: (context) async {
final newSpeed = await showDialog(
context: context,
builder: (context) => SpeedPicker(
initialValue: playerSettings.preferredDefaultSpeed,
),
);
if (newSpeed != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
preferredDefaultSpeed: newSpeed,
),
);
}
},
),
// preferred speed options
SettingsTile(
title: const Text('Speed Options'),
description: Text(
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
),
leading: const Icon(Icons.speed),
onPressed: (context) async {
final newSpeedOptions = await showDialog<List<double>?>(
context: context,
builder: (context) => SpeedOptionsPicker(
initialValue: playerSettings.speedOptions,
),
);
if (newSpeedOptions != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
speedOptions: newSpeedOptions..sort(),
),
);
}
},
),
],
),
// Playback Reporting
SettingsSection(
title: const Text('Playback Reporting'),
tiles: [
SettingsTile(
title: const Text('Minimum Position to Report'),
description: Text.rich(
TextSpan(
text: 'Do not report playback for the first ',
children: [
TextSpan(
text: playerSettings
.minimumPositionForReporting.smartBinaryFormat,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(text: ' of the book'),
],
),
),
leading: const Icon(Icons.timer),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: const Text('Ignore Playback Position Less Than'),
baseUnit: BaseUnit.second,
initialValue: playerSettings.minimumPositionForReporting,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
minimumPositionForReporting: newDuration,
),
);
}
},
),
// when to mark complete
SettingsTile(
title: const Text('Mark Complete When Time Left'),
description: Text.rich(
TextSpan(
text: 'Mark complete when less than ',
children: [
TextSpan(
text: playerSettings
.markCompleteWhenTimeLeft.smartBinaryFormat,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(text: ' left in the book'),
],
),
),
leading: const Icon(Icons.cloud_done),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: const Text('Mark Complete When Time Left'),
baseUnit: BaseUnit.second,
initialValue: playerSettings.markCompleteWhenTimeLeft,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
markCompleteWhenTimeLeft: newDuration,
),
);
}
},
),
// playback report interval
SettingsTile(
title: const Text('Playback Report Interval'),
description: Text.rich(
TextSpan(
text: 'Report progress every ',
children: [
TextSpan(
text: playerSettings
.playbackReportInterval.smartBinaryFormat,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(text: ' to the server'),
],
),
),
leading: const Icon(Icons.change_circle_outlined),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: const Text('Playback Report Interval'),
baseUnit: BaseUnit.second,
initialValue: playerSettings.playbackReportInterval,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
playbackReportInterval: newDuration,
),
);
}
},
),
],
),
// Display Settings
SettingsSection(
title: const Text('Display Settings'),
tiles: [
// show total progress
SettingsTile.switchTile(
title: const Text('Show Total Progress'),
leading: const Icon(Icons.show_chart),
description: const Text(
'Show the total progress of the book in the player',
),
initialValue:
playerSettings.expandedPlayerSettings.showTotalProgress,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings
.expandedPlayerSettings(showTotalProgress: value),
);
},
),
// show chapter progress
SettingsTile.switchTile(
title: const Text('Show Chapter Progress'),
leading: const Icon(Icons.show_chart),
description: const Text(
'Show the progress of the current chapter in the player',
),
initialValue:
playerSettings.expandedPlayerSettings.showChapterProgress,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
expandedPlayerSettings: playerSettings
.expandedPlayerSettings
.copyWith(showChapterProgress: value),
),
);
},
),
],
),
],
);
}
}
class TimeDurationSelector extends HookConsumerWidget {
const TimeDurationSelector({
super.key,
this.title = const Text('Select Duration'),
this.baseUnit = BaseUnit.second,
this.initialValue = Duration.zero,
});
final Widget title;
final BaseUnit baseUnit;
final Duration initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final duration = useState(initialValue);
return AlertDialog(
title: title,
content: DurationPicker(
duration: duration.value,
baseUnit: baseUnit,
onChange: (value) {
duration.value = value;
},
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(duration.value);
},
),
],
);
}
}
class SpeedPicker extends HookConsumerWidget {
const SpeedPicker({
super.key,
this.initialValue = 1,
});
final double initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final speedController =
useTextEditingController(text: initialValue.toString());
final speed = useState<double?>(initialValue);
return AlertDialog(
title: const Text('Select Speed'),
content: TextField(
controller: speedController,
onChanged: (value) => speed.value = double.tryParse(value),
onSubmitted: (value) {
Navigator.of(context).pop(speed.value);
},
autofocus: true,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Speed',
helper: Text(
'Enter the speed you want to set when playing for the first time',
),
),
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(speed.value);
},
),
],
);
}
}
class SpeedOptionsPicker extends HookConsumerWidget {
const SpeedOptionsPicker({
super.key,
this.initialValue = const [0.75, 1, 1.25, 1.5, 1.75, 2],
});
final List<double> initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final speedOptionAddController = useTextEditingController();
final speedOptions = useState<List<double>>(initialValue);
final focusNode = useFocusNode();
return AlertDialog(
title: const Text('Select Speed Options'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: speedOptions.value
.map(
(speed) => Chip(
label: Text('${speed}x'),
onDeleted: speed == 1
? null
: () {
speedOptions.value =
speedOptions.value.where((element) {
// speed option 1 can't be removed
return element != speed;
}).toList();
},
),
)
.toList()
..sort((a, b) {
// if (a.label == const Text('1x')) {
// return -1;
// } else if (b.label == const Text('1x')) {
// return 1;
// }
return a.label.toString().compareTo(b.label.toString());
}),
),
TextField(
focusNode: focusNode,
autofocus: true,
controller: speedOptionAddController,
onSubmitted: (value) {
final newSpeed = double.tryParse(value);
if (newSpeed != null && !speedOptions.value.contains(newSpeed)) {
speedOptions.value = [...speedOptions.value, newSpeed];
}
speedOptionAddController.clear();
focusNode.requestFocus();
},
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Add Speed Option',
helper: Text('Enter a new speed option to add'),
),
),
],
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(speedOptions.value);
},
),
],
);
}
}

View file

@ -46,6 +46,8 @@ class SimpleSettingsPage extends HookConsumerWidget {
],
),
),
// some padding at the bottom
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
],
),
);