diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 56e86d7..2fb0627 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -39,4 +39,9 @@ class AudiobookPlayer extends _$AudiobookPlayer { void notifyListeners() { ref.notifyListeners(); } + +Future setSpeed(double speed) async { + await state.setSpeed(speed); + notifyListeners(); + } } diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index ed2062c..9383a7b 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:whispering_pages/constants/sizes.dart'; @@ -267,17 +269,20 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); + final notifier = ref.watch(audiobookPlayerProvider.notifier); return TextButton( child: Text('${player.speed}x'), - // icon: const Icon(Icons.speed), onPressed: () { showModalBottomSheet( context: context, + barrierLabel: 'Select Speed', + constraints: const BoxConstraints( + maxHeight: 225, + ), builder: (context) { return SpeedSelector( onSpeedSelected: (speed) { - player.setSpeed(speed); - Navigator.of(context).pop(); + notifier.setSpeed(speed); }, ); }, @@ -300,19 +305,187 @@ class SpeedSelector extends HookConsumerWidget { final appSettings = ref.watch(appSettingsProvider); final speeds = appSettings.playerSettings.speedOptions; final currentSpeed = ref.watch(audiobookPlayerProvider).speed; - return SizedBox( - child: ListView.builder( - itemCount: speeds.length, - itemBuilder: (context, index) { - final speed = speeds[index]; - return ListTile( - title: Text(speed.toString()), - onTap: () { - onSpeedSelected(speed); - }, - trailing: currentSpeed == speed ? const Icon(Icons.check) : null, - ); - }, + final speedState = useState(currentSpeed); + + // hook the onSpeedSelected function to the state + useEffect( + () { + onSpeedSelected(speedState.value); + return null; + }, + [speedState.value], + ); + + // the speed options + const minSpeed = 0.1; + const maxSpeed = 4.0; + const speedIncrement = 0.05; + final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil(); + final availableSpeedsList = List.generate( + availableSpeeds, + (index) { + // need to round to 2 decimal place to avoid floating point errors + return double.parse( + (minSpeed + index * speedIncrement).toStringAsFixed(2), + ); + }, + ); + + final scrollController = FixedExtentScrollController( + initialItem: availableSpeedsList.indexOf(currentSpeed), + ); + const double itemExtent = 25; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + 'Playback Speed: ${speedState.value}x', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ), + 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, + 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.onBackground, + ), + // 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 + .onBackground, + ), + ), + ], + ), + ) + .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, + children: speeds + .map( + (speed) => Flexible( + // the text button should be highlighted if the speed is selected + child: TextButton( + style: speed == speedState.value + ? TextButton.styleFrom( + backgroundColor: Theme.of(context) + .colorScheme + .primaryContainer, + foregroundColor: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ) + : null, + onPressed: () async { + // animate the wheel to the selected speed + var index = availableSpeedsList.indexOf(speed); + // if the speed is not in the list + if (index == -1) { + // find the nearest speed + final nearestSpeed = availableSpeedsList.firstWhere( + (element) => element > speed, + orElse: () => availableSpeedsList.last, + ); + index = availableSpeedsList.indexOf(nearestSpeed); + } + await scrollController.animateToItem( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + + // call the onSpeedSelected function + speedState.value = speed; + }, + child: Text('$speed'), + ), + ), + ) + .toList(), + ), + const SizedBox( + height: 8, + ), + ], ), ); } diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 948e4f5..84d9141 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -29,7 +29,7 @@ class PlayerSettings with _$PlayerSettings { ExpandedPlayerSettings expandedPlayerSettings, @Default(1) double preferredDefaultVolume, @Default(1) double preferredDefaultSpeed, - @Default([0.8, 1, 1.25, 1.5, 1.75, 2]) List speedOptions, + @Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List speedOptions, @Default(Duration(minutes: 15)) Duration sleepTimer, }) = _PlayerSettings; diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index d1e693b..6643f4c 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -399,7 +399,7 @@ class _$PlayerSettingsImpl implements _PlayerSettings { this.expandedPlayerSettings = const ExpandedPlayerSettings(), this.preferredDefaultVolume = 1, this.preferredDefaultSpeed = 1, - final List speedOptions = const [0.8, 1, 1.25, 1.5, 1.75, 2], + final List speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2], this.sleepTimer = const Duration(minutes: 15)}) : _speedOptions = speedOptions; diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index d4f710c..764d207 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -41,7 +41,7 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => speedOptions: (json['speedOptions'] as List?) ?.map((e) => (e as num).toDouble()) .toList() ?? - const [0.8, 1, 1.25, 1.5, 1.75, 2], + const [0.75, 1, 1.25, 1.5, 1.75, 2], sleepTimer: json['sleepTimer'] == null ? const Duration(minutes: 15) : Duration(microseconds: (json['sleepTimer'] as num).toInt()), diff --git a/pubspec.lock b/pubspec.lock index c6f74a4..fc17b6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -720,6 +720,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + list_wheel_scroll_view_nls: + dependency: "direct main" + description: + name: list_wheel_scroll_view_nls + sha256: "47a6c27dac35768f2bcd0db05a31f04347ea116faf3529131d937cf130c36e91" + url: "https://pub.dev" + source: hosted + version: "0.0.3" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6b0e6d7..08cecf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,6 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used dependencies: animated_list_plus: ^0.5.2 animated_theme_switcher: ^2.0.10 - flutter_material_pickers: ^3.6.0 audio_session: ^0.1.19 audio_video_progress_bar: ^2.0.2 auto_scroll_text: ^0.0.7 @@ -46,6 +45,7 @@ dependencies: flutter_animate: ^4.5.0 flutter_cache_manager: ^3.3.2 flutter_hooks: ^0.20.5 + flutter_material_pickers: ^3.6.0 flutter_settings_ui: ^3.0.1 font_awesome_flutter: ^10.7.0 freezed_annotation: ^2.4.1 @@ -58,6 +58,7 @@ dependencies: just_audio: ^0.9.37 just_audio_background: ^0.0.1-beta.11 just_audio_media_kit: ^2.0.4 + list_wheel_scroll_view_nls: ^0.0.3 lottie: ^3.1.0 media_kit_libs_linux: any media_kit_libs_windows_audio: any