diff --git a/lib/app/player/components/sleep_time_indicator.dart b/lib/app/player/components/player_state_indicator.dart similarity index 52% rename from lib/app/player/components/sleep_time_indicator.dart rename to lib/app/player/components/player_state_indicator.dart index d63a012..32046eb 100644 --- a/lib/app/player/components/sleep_time_indicator.dart +++ b/lib/app/player/components/player_state_indicator.dart @@ -4,17 +4,30 @@ import 'package:syncara/app/player/player_menu_sheet.dart'; import 'package:syncara/extensions.dart'; import 'package:syncara/provider/player_provider.dart'; -class SleepTimeIndicator extends StatelessWidget { - const SleepTimeIndicator({super.key}) : _static = false; +// https://github.com/material-components/material-components-android/blob/master/docs/components/Button.md#connected-button-group +// TODO There's no flutter component. Push this upstream someday +class PlayerStateIndicator extends StatelessWidget { + const PlayerStateIndicator({super.key}) : _static = false; final bool _static; - const SleepTimeIndicator.static({super.key}) : _static = true; + const PlayerStateIndicator.static({super.key}) : _static = true; @override Widget build(BuildContext context) { - if (_static) return _sleepTimerStatic(context); - return _sleepTimerIndicator(context); + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + if (_static) ...{ + Flexible(child: _sleepTimerStatic(context)), + } else ...{ + Flexible(child: _sleepTimerIndicator(context)), + Flexible(child: _speedIndicator(context)), + } + ], + ); } Widget _sleepTimerIndicator(BuildContext context) { @@ -36,6 +49,24 @@ class SleepTimeIndicator extends StatelessWidget { ); } + Widget _speedIndicator(BuildContext context) { + return AnimatedSize( + duration: Durations.short3, + child: StreamBuilder( + stream: context.read().player.speedStream, + initialData: context.read().player.speed, + builder: (context, snapshot) { + if (snapshot.data == 1.0) return const SizedBox(); + return FilledButton.tonalIcon( + onPressed: () => PlayerMenuSheet.setSpeedPopup(context), + icon: const Icon(Icons.speed_rounded), + label: Text("${snapshot.data}x"), + ); + }, + ), + ); + } + /// This shows a static indicator Widget _sleepTimerStatic(BuildContext context) { return StreamBuilder( diff --git a/lib/app/player/large_player_sheet.dart b/lib/app/player/large_player_sheet.dart index dc260bd..f880182 100644 --- a/lib/app/player/large_player_sheet.dart +++ b/lib/app/player/large_player_sheet.dart @@ -6,7 +6,7 @@ import 'package:syncara/app/player/components/action_buttons.dart'; import 'package:syncara/app/player/components/artwork.dart'; import 'package:syncara/app/player/components/lyrics.dart'; import 'package:syncara/app/player/components/seekbar.dart'; -import 'package:syncara/app/player/components/sleep_time_indicator.dart'; +import 'package:syncara/app/player/components/player_state_indicator.dart'; import 'package:syncara/app/player/player_menu_sheet.dart'; import 'package:syncara/app/player/player_queue_sheet.dart'; import 'package:syncara/model/media.dart'; @@ -89,7 +89,7 @@ class _LargePlayerSheetState extends State spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ - SleepTimeIndicator(), + PlayerStateIndicator(), ], ), ), diff --git a/lib/app/player/mini_player_sheet.dart b/lib/app/player/mini_player_sheet.dart index ffde6bc..6ca366c 100644 --- a/lib/app/player/mini_player_sheet.dart +++ b/lib/app/player/mini_player_sheet.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:network_to_file_image/network_to_file_image.dart'; import 'package:provider/provider.dart'; import 'package:syncara/app/app_theme.dart'; -import 'package:syncara/app/player/components/sleep_time_indicator.dart'; +import 'package:syncara/app/player/components/player_state_indicator.dart'; import 'package:syncara/app/player/large_player_sheet.dart'; import 'package:syncara/clients/media_client.dart'; import 'package:syncara/model/media.dart'; @@ -61,22 +61,23 @@ class MiniPlayerSheet extends StatelessWidget { // Progress Indicator Selector( selector: (_, provider) => provider.buffering, - builder: (_, buffering, progressIndicator) { - if (!buffering) return progressIndicator!; - return LinearProgressIndicator( - minHeight: adaptiveIndicatorHeight, - ); - }, - child: StreamBuilder( + builder: (_, buffering, __) => StreamBuilder( stream: context.read().player.positionStream, builder: (context, snapshot) { + double? progress; final duration = nowPlaying.durationMs; - var progress = (duration != null && snapshot.hasData) - ? snapshot.requireData.inMilliseconds / duration - : null; - return LinearProgressIndicator( - minHeight: adaptiveIndicatorHeight, - value: progress, + if (!buffering && duration != null && snapshot.hasData) { + progress = snapshot.requireData.inMilliseconds / duration; + } + + return StreamBuilder( + stream: context.read().player.speedStream, + initialData: context.read().player.speed, + builder: (_, speed) => LinearProgressIndicator( + minHeight: adaptiveIndicatorHeight, + color: speed.data == 1.0 ? null : Colors.redAccent, + value: progress, + ), ); }, ), @@ -170,7 +171,7 @@ class MiniPlayerSheet extends StatelessWidget { url: media.thumbnailStd, file: MediaClient().thumbnailFile(media.thumbnailStd), ), - child: const SleepTimeIndicator.static(), + child: const PlayerStateIndicator.static(), ), ); } diff --git a/lib/app/player/player_menu_sheet.dart b/lib/app/player/player_menu_sheet.dart index 6d0c5b5..433caa8 100644 --- a/lib/app/player/player_menu_sheet.dart +++ b/lib/app/player/player_menu_sheet.dart @@ -25,12 +25,12 @@ class PlayerMenuSheet extends StatelessWidget { ), ListTile( onTap: () { - setSleepTimerPopup(context).then((ok) { + setSpeedPopup(context).then((ok) { if (ok && context.mounted) Navigator.pop(context); }); }, - leading: const Icon(Icons.bedtime_rounded), - title: const Text("Sleep Timer"), + leading: const Icon(Icons.speed_rounded), + title: const Text("Playback Speed"), ), ], ), @@ -72,4 +72,29 @@ class PlayerMenuSheet extends StatelessWidget { return true; } } + + static Future setSpeedPopup(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (_) => ChoiceDialog( + title: "Playback Speed", + selected: context.read().player.speed, + icon: const Icon(Icons.speed_rounded), + options: { + for (final i in [0.25, 0.5, 0.75]) ...{ + "${i}x": i, + }, + "1x (Default)": 1, + for (final i in [1.25, 1.5, 1.75, 2.0]) ...{ + "${i}x": i, + }, + }, + ), + ); + + if (!context.mounted || result == null) return false; + + context.read().setPlaybackSpeed(result); + return true; + } } diff --git a/lib/provider/player_provider.dart b/lib/provider/player_provider.dart index 56703ef..c5a1779 100644 --- a/lib/provider/player_provider.dart +++ b/lib/provider/player_provider.dart @@ -260,6 +260,10 @@ class PlayerProvider extends ChangeNotifier { notifyListeners(); } + void setPlaybackSpeed(double speed) { + player.setSpeed(speed); + } + /// Passing null duration & afterSong will cancel the timer void setSleepTimer({Duration? duration, bool? afterSong}) { if (duration == null && afterSong == null) {