Skip to content

Commit

Permalink
feat: Playback Speed Controls
Browse files Browse the repository at this point in the history
closes #26
  • Loading branch information
khaled-0 committed Dec 28, 2024
1 parent d36f0e6 commit 70ea3ad
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -36,6 +49,24 @@ class SleepTimeIndicator extends StatelessWidget {
);
}

Widget _speedIndicator(BuildContext context) {
return AnimatedSize(
duration: Durations.short3,
child: StreamBuilder(
stream: context.read<PlayerProvider>().player.speedStream,
initialData: context.read<PlayerProvider>().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(
Expand Down
4 changes: 2 additions & 2 deletions lib/app/player/large_player_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -89,7 +89,7 @@ class _LargePlayerSheetState extends State<LargePlayerSheet>
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SleepTimeIndicator(),
PlayerStateIndicator(),
],
),
),
Expand Down
31 changes: 16 additions & 15 deletions lib/app/player/mini_player_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,22 +61,23 @@ class MiniPlayerSheet extends StatelessWidget {
// Progress Indicator
Selector<PlayerProvider, bool>(
selector: (_, provider) => provider.buffering,
builder: (_, buffering, progressIndicator) {
if (!buffering) return progressIndicator!;
return LinearProgressIndicator(
minHeight: adaptiveIndicatorHeight,
);
},
child: StreamBuilder<Duration>(
builder: (_, buffering, __) => StreamBuilder<Duration>(
stream: context.read<PlayerProvider>().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<PlayerProvider>().player.speedStream,
initialData: context.read<PlayerProvider>().player.speed,
builder: (_, speed) => LinearProgressIndicator(
minHeight: adaptiveIndicatorHeight,
color: speed.data == 1.0 ? null : Colors.redAccent,
value: progress,
),
);
},
),
Expand Down Expand Up @@ -170,7 +171,7 @@ class MiniPlayerSheet extends StatelessWidget {
url: media.thumbnailStd,
file: MediaClient().thumbnailFile(media.thumbnailStd),
),
child: const SleepTimeIndicator.static(),
child: const PlayerStateIndicator.static(),
),
);
}
Expand Down
31 changes: 28 additions & 3 deletions lib/app/player/player_menu_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
],
),
Expand Down Expand Up @@ -72,4 +72,29 @@ class PlayerMenuSheet extends StatelessWidget {
return true;
}
}

static Future<bool> setSpeedPopup(BuildContext context) async {
final result = await showDialog<double?>(
context: context,
builder: (_) => ChoiceDialog<double>(
title: "Playback Speed",
selected: context.read<PlayerProvider>().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<PlayerProvider>().setPlaybackSpeed(result);
return true;
}
}
4 changes: 4 additions & 0 deletions lib/provider/player_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 70ea3ad

Please sign in to comment.