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( 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( 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) ], ), ), ), ); } }