mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
feat: add player settings page and enhance settings UI (#33)
This commit is contained in:
parent
8049a660e6
commit
150e5c9025
12 changed files with 761 additions and 157 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
34
lib/settings/view/buttons.dart
Normal file
34
lib/settings/view/buttons.dart
Normal 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
410
lib/settings/view/player_settings_page.dart
Normal file
410
lib/settings/view/player_settings_page.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,8 @@ class SimpleSettingsPage extends HookConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
// some padding at the bottom
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue