spotube/lib/components/player/player_controls.dart
Kingkor Roy Tirtho 312f7fbe77 refactor(playback): new immutable queue based playback manager
Dropping support for search format, track match algorithm in favor of server track cache and alternative track source
2023-02-02 18:43:12 +06:00

234 lines
9.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/primitive_utils.dart';
class PlayerControls extends HookConsumerWidget {
final Color? iconColor;
PlayerControls({
this.iconColor,
Key? key,
}) : super(key: key);
final logger = getLogger(PlayerControls);
static FocusNode focusNode = FocusNode();
@override
Widget build(BuildContext context, ref) {
final shortcuts = useMemoized(
() => {
const SingleActivator(LogicalKeyboardKey.arrowRight):
SeekIntent(ref, true),
const SingleActivator(LogicalKeyboardKey.arrowLeft):
SeekIntent(ref, false),
},
[ref]);
final actions = useMemoized(
() => {
SeekIntent: SeekAction(),
},
[]);
final playlist = ref.watch(PlaylistQueueNotifier.provider);
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final playing = useStream(PlaylistQueueNotifier.playing).data ?? false;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (focusNode.canRequestFocus) {
focusNode.requestFocus();
}
},
child: FocusableActionDetector(
focusNode: focusNode,
shortcuts: shortcuts,
actions: actions,
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
children: [
HookBuilder(
builder: (context) {
final duration =
useStream(PlaylistQueueNotifier.duration).data ??
Duration.zero;
final positionSnapshot =
useStream(PlaylistQueueNotifier.position);
final position = positionSnapshot.data ?? Duration.zero;
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60));
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60));
final currentMinutes = PrimitiveUtils.zeroPadNumStr(
position.inMinutes.remainder(60));
final currentSeconds = PrimitiveUtils.zeroPadNumStr(
position.inSeconds.remainder(60));
final sliderMax = duration.inSeconds;
final sliderValue = position.inSeconds;
final progressStatic =
(sliderMax == 0 || sliderValue > sliderMax)
? 0
: sliderValue / sliderMax;
final progress = useState<num>(
useMemoized(() => progressStatic, []),
);
useEffect(() {
progress.value = progressStatic;
return null;
}, [progressStatic]);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (positionSnapshot.hasData &&
duration == Duration.zero) {
await Future.delayed(const Duration(milliseconds: 200));
await playlistNotifier.pause();
await Future.delayed(const Duration(milliseconds: 400));
await playlistNotifier.resume();
}
});
return null;
}, [positionSnapshot.hasData, duration]);
return Column(
children: [
PlatformTooltip(
message: "Slide to seek forward or backward",
child: PlatformSlider(
// cannot divide by zero
// there's an edge case for value being bigger
// than total duration. Keeping it resolved
value: progress.value.toDouble(),
onChanged: (v) {
progress.value = v;
},
onChangeEnd: (value) async {
await playlistNotifier.seek(
Duration(
seconds: (value * sliderMax).toInt(),
),
);
},
activeColor: iconColor,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
PlatformText(
"$currentMinutes:$currentSeconds",
),
PlatformText("$totalMinutes:$totalSeconds"),
],
),
),
],
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PlatformIconButton(
tooltip: playlistNotifier.isShuffled
? "Unshuffle playlist"
: "Shuffle playlist",
icon: Icon(
SpotubeIcons.shuffle,
color: playlistNotifier.isShuffled
? PlatformTheme.of(context).primaryColor
: null,
),
onPressed: playlist == null
? null
: () {
if (playlistNotifier.isShuffled) {
playlistNotifier.unshuffle();
} else {
playlistNotifier.shuffle();
}
},
),
PlatformIconButton(
tooltip: "Previous track",
icon: Icon(
SpotubeIcons.skipBack,
color: iconColor,
),
onPressed: playlistNotifier.previous,
),
PlatformIconButton(
tooltip: playing ? "Pause playback" : "Resume playback",
icon: playlist?.isLoading == true
? const SizedBox(
height: 20,
width: 20,
child: PlatformCircularProgressIndicator(),
)
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
color: iconColor,
),
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
),
PlatformIconButton(
tooltip: "Next track",
icon: Icon(
SpotubeIcons.skipForward,
color: iconColor,
),
onPressed: playlistNotifier.next,
),
PlatformIconButton(
tooltip: "Stop playback",
icon: Icon(
SpotubeIcons.stop,
color: iconColor,
),
onPressed: playlist != null ? playlistNotifier.stop : null,
),
// PlatformIconButton(
// tooltip:
// !playlist.isLoop ? "Loop Track" : "Repeat playlist",
// icon: Icon(
// playlist.isLoop
// ? SpotubeIcons.repeatOne
// : SpotubeIcons.repeat,
// ),
// onPressed:
// playlist.track == null || playlist.playlist == null
// ? null
// : () {
// playlist.setIsLoop(!playlist.isLoop);
// },
// ),
],
),
const SizedBox(height: 5)
],
),
),
),
);
}
}