diff --git a/.vscode/settings.json b/.vscode/settings.json index fa4f1f51..c4917255 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "danceability", "instrumentalness", "Mpris", + "riverpod", "speechiness", "Spotube", "winget" diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index b4984c51..7ae4fa82 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -48,7 +48,6 @@ class PlayerControls extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; - final buffering = useStream(audioPlayer.bufferingStream).data ?? true; final theme = Theme.of(context); final isDominantColorDark = ThemeData.estimateBrightnessForColor( @@ -89,215 +88,208 @@ class PlayerControls extends HookConsumerWidget { iconSize: compact ? 18 : 24, ); - return RepaintBoundary( - child: 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: [ - if (!compact) - HookBuilder( - builder: (context) { - final ( - :bufferProgress, - :duration, - :position, - :progressStatic - ) = useProgress(ref); + 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: [ + if (!compact) + HookBuilder( + builder: (context) { + final ( + :bufferProgress, + :duration, + :position, + :progressStatic + ) = useProgress(ref); - 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 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 progress = useState( - useMemoized(() => progressStatic, []), - ); + final progress = useState( + useMemoized(() => progressStatic, []), + ); - useEffect(() { - progress.value = progressStatic; - return null; - }, [progressStatic]); + useEffect(() { + progress.value = progressStatic; + return null; + }, [progressStatic]); - return Column( - children: [ - Tooltip( - message: context.l10n.slide_to_seek, - child: Slider( - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: progress.value.toDouble(), - secondaryTrackValue: bufferProgress, - onChanged: - playlist.isFetching == true || buffering - ? null - : (v) { - progress.value = v; - }, - onChangeEnd: (value) async { - await audioPlayer.seek( - Duration( - seconds: - (value * duration.inSeconds).toInt(), - ), - ); - }, - activeColor: sliderColor, - secondaryActiveColor: - sliderColor.withOpacity(0.2), - inactiveColor: sliderColor.withOpacity(0.15), + return Column( + children: [ + Tooltip( + message: context.l10n.slide_to_seek, + child: Slider( + // cannot divide by zero + // there's an edge case for value being bigger + // than total duration. Keeping it resolved + value: progress.value.toDouble(), + secondaryTrackValue: bufferProgress, + onChanged: playlist.isFetching == true + ? null + : (v) { + progress.value = v; + }, + onChangeEnd: (value) async { + await audioPlayer.seek( + Duration( + seconds: (value * duration.inSeconds).toInt(), + ), + ); + }, + activeColor: sliderColor, + secondaryActiveColor: sliderColor.withOpacity(0.2), + inactiveColor: sliderColor.withOpacity(0.15), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: DefaultTextStyle( + style: theme.textTheme.bodySmall!.copyWith( + color: palette?.dominantColor?.bodyTextColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("$currentMinutes:$currentSeconds"), + Text("$totalMinutes:$totalSeconds"), + ], ), ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: DefaultTextStyle( - style: theme.textTheme.bodySmall!.copyWith( - color: palette?.dominantColor?.bodyTextColor, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text("$currentMinutes:$currentSeconds"), - Text("$totalMinutes:$totalSeconds"), - ], - ), - ), - ), - ], - ); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - StreamBuilder( - stream: audioPlayer.shuffledStream, - builder: (context, snapshot) { - final shuffled = snapshot.data ?? false; - return IconButton( - tooltip: shuffled - ? context.l10n.unshuffle_playlist - : context.l10n.shuffle_playlist, - icon: const Icon(SpotubeIcons.shuffle), - style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlist.isFetching == true || buffering - ? null - : () { - if (shuffled) { - audioPlayer.setShuffle(false); - } else { - audioPlayer.setShuffle(true); - } - }, - ); - }), - IconButton( - tooltip: context.l10n.previous_track, - icon: const Icon(SpotubeIcons.skipBack), - style: buttonStyle, - onPressed: playlist.isFetching == true || buffering - ? null - : playlistNotifier.previous, - ), - IconButton( - tooltip: playing - ? context.l10n.pause_playback - : context.l10n.resume_playback, - icon: playlist.isFetching == true - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: accentColor?.titleTextColor ?? - theme.colorScheme.onPrimary, - ), - ) - : Icon( - playing ? SpotubeIcons.pause : SpotubeIcons.play, - ), - style: resumePauseStyle, - onPressed: playlist.isFetching == true - ? null - : Actions.handler( - context, - PlayPauseIntent(ref), - ), - ), - IconButton( - tooltip: context.l10n.next_track, - icon: const Icon(SpotubeIcons.skipForward), - style: buttonStyle, - onPressed: playlist.isFetching == true || buffering - ? null - : playlistNotifier.next, - ), - StreamBuilder( - stream: audioPlayer.loopModeStream, - builder: (context, snapshot) { - final loopMode = - snapshot.data ?? PlaybackLoopMode.none; - return IconButton( - tooltip: loopMode == PlaybackLoopMode.one - ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all - ? context.l10n.repeat_playlist - : null, - icon: Icon( - loopMode == PlaybackLoopMode.one - ? SpotubeIcons.repeatOne - : SpotubeIcons.repeat, - ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all - ? activeButtonStyle - : buttonStyle, - onPressed: playlist.isFetching == true || buffering - ? null - : () async { - switch (await audioPlayer.loopMode) { - case PlaybackLoopMode.all: - audioPlayer - .setLoopMode(PlaybackLoopMode.one); - break; - case PlaybackLoopMode.one: - audioPlayer - .setLoopMode(PlaybackLoopMode.none); - break; - case PlaybackLoopMode.none: - audioPlayer - .setLoopMode(PlaybackLoopMode.all); - break; - } - }, - ); - }), - ], + ), + ], + ); + }, ), - const SizedBox(height: 5) - ], - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + StreamBuilder( + stream: audioPlayer.shuffledStream, + builder: (context, snapshot) { + final shuffled = snapshot.data ?? false; + return IconButton( + tooltip: shuffled + ? context.l10n.unshuffle_playlist + : context.l10n.shuffle_playlist, + icon: const Icon(SpotubeIcons.shuffle), + style: shuffled ? activeButtonStyle : buttonStyle, + onPressed: playlist.isFetching == true + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, + ); + }), + IconButton( + tooltip: context.l10n.previous_track, + icon: const Icon(SpotubeIcons.skipBack), + style: buttonStyle, + onPressed: playlist.isFetching == true + ? null + : playlistNotifier.previous, + ), + IconButton( + tooltip: playing + ? context.l10n.pause_playback + : context.l10n.resume_playback, + icon: playlist.isFetching == true + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: accentColor?.titleTextColor ?? + theme.colorScheme.onPrimary, + ), + ) + : Icon( + playing ? SpotubeIcons.pause : SpotubeIcons.play, + ), + style: resumePauseStyle, + onPressed: playlist.isFetching == true + ? null + : Actions.handler( + context, + PlayPauseIntent(ref), + ), + ), + IconButton( + tooltip: context.l10n.next_track, + icon: const Icon(SpotubeIcons.skipForward), + style: buttonStyle, + onPressed: playlist.isFetching == true + ? null + : playlistNotifier.next, + ), + StreamBuilder( + stream: audioPlayer.loopModeStream, + builder: (context, snapshot) { + final loopMode = snapshot.data ?? PlaybackLoopMode.none; + return IconButton( + tooltip: loopMode == PlaybackLoopMode.one + ? context.l10n.loop_track + : loopMode == PlaybackLoopMode.all + ? context.l10n.repeat_playlist + : null, + icon: Icon( + loopMode == PlaybackLoopMode.one + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + ), + style: loopMode == PlaybackLoopMode.one || + loopMode == PlaybackLoopMode.all + ? activeButtonStyle + : buttonStyle, + onPressed: playlist.isFetching == true + ? null + : () async { + switch (await audioPlayer.loopMode) { + case PlaybackLoopMode.all: + audioPlayer + .setLoopMode(PlaybackLoopMode.one); + break; + case PlaybackLoopMode.one: + audioPlayer + .setLoopMode(PlaybackLoopMode.none); + break; + case PlaybackLoopMode.none: + audioPlayer + .setLoopMode(PlaybackLoopMode.all); + break; + } + }, + ); + }), + ], + ), + const SizedBox(height: 5) + ], ), ), ), diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index e51d11ef..f4984ad2 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -62,99 +62,95 @@ class PlayerOverlay extends HookConsumerWidget { child: AnimatedOpacity( duration: const Duration(milliseconds: 250), opacity: canShow ? 1 : 0, - child: RepaintBoundary( - child: Material( - type: MaterialType.transparency, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - HookBuilder( - builder: (context) { - final progress = useProgress(ref); - // animated - return TweenAnimationBuilder( - duration: const Duration(milliseconds: 250), - tween: Tween( - begin: 0, - end: progress.progressStatic, - ), - builder: (context, value, child) { - return LinearProgressIndicator( - value: value, - minHeight: 2, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.primary, - ), - ); - }, - ); - }, - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => - GoRouter.of(context).push("/player"), - child: PlayerTrackDetails( - albumArt: albumArt, - color: textColor, - ), + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + HookBuilder( + builder: (context) { + final progress = useProgress(ref); + // animated + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 250), + tween: Tween( + begin: 0, + end: progress.progressStatic, + ), + builder: (context, value, child) { + return LinearProgressIndicator( + value: value, + minHeight: 2, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + theme.colorScheme.primary, + ), + ); + }, + ); + }, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => + GoRouter.of(context).push("/player"), + child: PlayerTrackDetails( + albumArt: albumArt, + color: textColor, ), ), ), - Row( - children: [ - IconButton( - icon: Icon( - SpotubeIcons.skipBack, - color: textColor, - ), - onPressed: playlistNotifier.previous, + ), + Row( + children: [ + IconButton( + icon: Icon( + SpotubeIcons.skipBack, + color: textColor, ), - Consumer( - builder: (context, ref, _) { - return IconButton( - icon: playlist.isFetching - ? const SizedBox( - height: 20, - width: 20, - child: - CircularProgressIndicator(), - ) - : Icon( - playing - ? SpotubeIcons.pause - : SpotubeIcons.play, - color: textColor, - ), - onPressed: - Actions.handler( - context, - PlayPauseIntent(ref), - ), - ); - }, + onPressed: playlistNotifier.previous, + ), + Consumer( + builder: (context, ref, _) { + return IconButton( + icon: playlist.isFetching + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + playing + ? SpotubeIcons.pause + : SpotubeIcons.play, + color: textColor, + ), + onPressed: Actions.handler( + context, + PlayPauseIntent(ref), + ), + ); + }, + ), + IconButton( + icon: Icon( + SpotubeIcons.skipForward, + color: textColor, ), - IconButton( - icon: Icon( - SpotubeIcons.skipForward, - color: textColor, - ), - onPressed: playlistNotifier.next, - ), - ], - ), - ], - ), + onPressed: playlistNotifier.next, + ), + ], + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/hooks/use_progress.dart b/lib/hooks/use_progress.dart index 8ba2f336..40429190 100644 --- a/lib/hooks/use_progress.dart +++ b/lib/hooks/use_progress.dart @@ -1,6 +1,5 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; ({ @@ -9,21 +8,29 @@ import 'package:spotube/services/audio_player/audio_player.dart'; Duration duration, double bufferProgress }) useProgress(WidgetRef ref) { - final playlist = ref.watch(ProxyPlaylistNotifier.provider); - final bufferProgress = useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0; + Duration audioPlayerDuration = Duration.zero; + Duration audioPlayerPosition = Duration.zero; + // Duration future is needed for getting the duration of the song // as stream can be null when no event occurs (Mostly needed for android) - final durationFuture = useFuture(audioPlayer.duration); - final duration = useStream(audioPlayer.durationStream).data ?? - durationFuture.data ?? - Duration.zero; + audioPlayer.duration.then((value) { + if (value != null) { + audioPlayerDuration = value; + } + }); - final positionFuture = useFuture(audioPlayer.position); - final position = useState(positionFuture.data ?? Duration.zero); + audioPlayer.position.then((value) { + if (value != null) { + audioPlayerPosition = value; + } + }); + final position = useState(audioPlayerPosition); + final duration = + useStream(audioPlayer.durationStream).data ?? audioPlayerDuration; final sliderMax = duration.inSeconds; final sliderValue = position.value.inSeconds; @@ -38,7 +45,7 @@ import 'package:spotube/services/audio_player/audio_player.dart'; lastPosition = event; position.value = event; }).cancel; - }, []); + }, [audioPlayerPosition, audioPlayerDuration]); return ( progressStatic: