feat: merge floating player with nav bar and nav bar translucent bg

This commit is contained in:
Kingkor Roy Tirtho 2023-03-10 19:19:55 +06:00
parent 67380f6876
commit a90261ed19
15 changed files with 255 additions and 156 deletions

View File

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -58,28 +59,24 @@ class PlayerControls extends HookConsumerWidget {
children: [ children: [
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
final duration = final progressObj = useProgress(ref);
useStream(PlaylistQueueNotifier.duration).data ??
Duration.zero; final progressStatic = progressObj.item1;
final positionSnapshot = final position = progressObj.item2;
useStream(PlaylistQueueNotifier.position); final duration = progressObj.item3;
final position = positionSnapshot.data ?? Duration.zero;
final totalMinutes = PrimitiveUtils.zeroPadNumStr( final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60)); duration.inMinutes.remainder(60),
);
final totalSeconds = PrimitiveUtils.zeroPadNumStr( final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60)); duration.inSeconds.remainder(60),
);
final currentMinutes = PrimitiveUtils.zeroPadNumStr( final currentMinutes = PrimitiveUtils.zeroPadNumStr(
position.inMinutes.remainder(60)); position.inMinutes.remainder(60),
);
final currentSeconds = PrimitiveUtils.zeroPadNumStr( final currentSeconds = PrimitiveUtils.zeroPadNumStr(
position.inSeconds.remainder(60)); 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>( final progress = useState<num>(
useMemoized(() => progressStatic, []), useMemoized(() => progressStatic, []),
@ -90,20 +87,6 @@ class PlayerControls extends HookConsumerWidget {
return null; return null;
}, [progressStatic]); }, [progressStatic]);
// this is a hack to fix duration not being updated
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( return Column(
children: [ children: [
Tooltip( Tooltip(
@ -121,7 +104,7 @@ class PlayerControls extends HookConsumerWidget {
onChangeEnd: (value) async { onChangeEnd: (value) async {
await playlistNotifier.seek( await playlistNotifier.seek(
Duration( Duration(
seconds: (value * sliderMax).toInt(), seconds: (value * duration.inSeconds).toInt(),
), ),
); );
}, },

View File

@ -7,8 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_track_details.dart'; import 'package:spotube/components/player/player_track_details.dart';
import 'package:spotube/hooks/use_palette_color.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -22,7 +22,6 @@ class PlayerOverlay extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final paletteColor = usePaletteColor(albumArt, ref);
final canShow = ref.watch( final canShow = ref.watch(
PlaylistQueueNotifier.provider.select((s) => s != null), PlaylistQueueNotifier.provider.select((s) => s != null),
); );
@ -31,6 +30,13 @@ class PlayerOverlay extends HookConsumerWidget {
final playing = useStream(PlaylistQueueNotifier.playing).data ?? final playing = useStream(PlaylistQueueNotifier.playing).data ??
PlaylistQueueNotifier.isPlaying; PlaylistQueueNotifier.isPlaying;
final textColor = Theme.of(context).colorScheme.primary;
const radius = BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
);
return GestureDetector( return GestureDetector(
onVerticalDragEnd: (details) { onVerticalDragEnd: (details) {
int sensitivity = 8; int sensitivity = 8;
@ -40,26 +46,49 @@ class PlayerOverlay extends HookConsumerWidget {
} }
}, },
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(5), borderRadius: radius,
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: canShow ? 50 : 0, height: canShow ? 53 : 0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: paletteColor.color.withOpacity(.7), color: Theme.of(context)
border: Border.all( .colorScheme
color: paletteColor.titleTextColor, .secondaryContainer
width: 2, .withOpacity(.8),
), borderRadius: radius,
borderRadius: BorderRadius.circular(5),
), ),
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
opacity: canShow ? 1 : 0, opacity: canShow ? 1 : 0,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
HookBuilder(
builder: (context) {
final progress = useProgress(ref);
// animated
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 250),
tween: Tween<double>(begin: 0, end: progress.item1),
builder: (context, value, child) {
return LinearProgressIndicator(
value: value,
minHeight: 2,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).colorScheme.primary,
),
);
},
);
},
),
Expanded(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -67,10 +96,11 @@ class PlayerOverlay extends HookConsumerWidget {
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
onTap: () => GoRouter.of(context).push("/player"), onTap: () =>
GoRouter.of(context).push("/player"),
child: PlayerTrackDetails( child: PlayerTrackDetails(
albumArt: albumArt, albumArt: albumArt,
color: paletteColor.bodyTextColor, color: textColor,
), ),
), ),
), ),
@ -80,7 +110,7 @@ class PlayerOverlay extends HookConsumerWidget {
IconButton( IconButton(
icon: Icon( icon: Icon(
SpotubeIcons.skipBack, SpotubeIcons.skipBack,
color: paletteColor.bodyTextColor, color: textColor,
), ),
onPressed: playlistNotifier.previous, onPressed: playlistNotifier.previous,
), ),
@ -97,7 +127,7 @@ class PlayerOverlay extends HookConsumerWidget {
playing playing
? SpotubeIcons.pause ? SpotubeIcons.pause
: SpotubeIcons.play, : SpotubeIcons.play,
color: paletteColor.bodyTextColor, color: textColor,
), ),
onPressed: Actions.handler<PlayPauseIntent>( onPressed: Actions.handler<PlayPauseIntent>(
context, context,
@ -109,7 +139,7 @@ class PlayerOverlay extends HookConsumerWidget {
IconButton( IconButton(
icon: Icon( icon: Icon(
SpotubeIcons.skipForward, SpotubeIcons.skipForward,
color: paletteColor.bodyTextColor, color: textColor,
), ),
onPressed: playlistNotifier.next, onPressed: playlistNotifier.next,
), ),
@ -118,6 +148,9 @@ class PlayerOverlay extends HookConsumerWidget {
], ],
), ),
), ),
],
),
),
), ),
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
@ -20,13 +21,17 @@ class PlayerTrackDetails extends HookConsumerWidget {
return Row( return Row(
children: [ children: [
if (albumArt != null) if (playback != null)
Padding( Container(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(6),
constraints: const BoxConstraints(
maxWidth: 70,
maxHeight: 70,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
path: albumArt!, path: albumArt ?? "",
height: 50,
width: 50,
placeholder: (context, url) { placeholder: (context, url) {
return Assets.albumPlaceholder.image( return Assets.albumPlaceholder.image(
height: 50, height: 50,
@ -35,23 +40,40 @@ class PlayerTrackDetails extends HookConsumerWidget {
}, },
), ),
), ),
),
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
Flexible( Flexible(
child: Text( child: Column(
playback?.activeTrack.name ?? "Not playing", crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
playback?.activeTrack.name ?? "",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color), style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: color,
),
),
Text(
TypeConversionUtils.artists_X_String<Artist>(
playback?.activeTrack.artists ?? [],
),
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: color),
)
],
), ),
), ),
// title of the currently playing track
if (breakpoint.isMoreThan(Breakpoints.md)) if (breakpoint.isMoreThan(Breakpoints.md))
Flexible( Flexible(
flex: 1, flex: 1,
child: Column( child: Column(
children: [ children: [
Text( Text(
playback?.activeTrack.name ?? "Not playing", playback?.activeTrack.name ?? "",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color), style: TextStyle(fontWeight: FontWeight.bold, color: color),
), ),

View File

@ -42,10 +42,7 @@ class BottomPlayer extends HookConsumerWidget {
if (layoutMode == LayoutMode.compact || if (layoutMode == LayoutMode.compact ||
(breakpoint.isLessThanOrEqualTo(Breakpoints.md) && (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
layoutMode == LayoutMode.adaptive)) { layoutMode == LayoutMode.adaptive)) {
return Padding( return PlayerOverlay(albumArt: albumArt);
padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8, top: 0),
child: PlayerOverlay(albumArt: albumArt),
);
} }
return DecoratedBox( return DecoratedBox(

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:curved_navigation_bar/curved_navigation_bar.dart'; import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -45,8 +47,14 @@ class SpotubeNavigationBar extends HookConsumerWidget {
(breakpoint.isMoreThan(Breakpoints.sm) && (breakpoint.isMoreThan(Breakpoints.sm) &&
layoutMode == LayoutMode.adaptive)) return const SizedBox(); layoutMode == LayoutMode.adaptive)) return const SizedBox();
return CurvedNavigationBar( return ClipRect(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: CurvedNavigationBar(
backgroundColor: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.72),
buttonBackgroundColor: buttonColor, buttonBackgroundColor: buttonColor,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
height: 50, height: 50,
@ -83,6 +91,8 @@ class SpotubeNavigationBar extends HookConsumerWidget {
} }
onSelectedIndexChanged(i); onSelectedIndexChanged(i);
}, },
),
),
); );
} }
} }

View File

@ -180,6 +180,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
); );
return SafeArea( return SafeArea(
bottom: false,
child: Scaffold( child: Scaffold(
appBar: kIsDesktop appBar: kIsDesktop
? PageWindowTitleBar( ? PageWindowTitleBar(

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:tuple/tuple.dart';
Tuple3<double, Duration, Duration> useProgress(WidgetRef ref) {
ref.watch(PlaylistQueueNotifier.provider);
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final duration =
useStream(PlaylistQueueNotifier.duration).data ?? Duration.zero;
final positionSnapshot = useStream(PlaylistQueueNotifier.position);
final position = positionSnapshot.data ?? Duration.zero;
final sliderMax = duration.inSeconds;
final sliderValue = position.inSeconds;
// this is a hack to fix duration not being updated
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 Tuple3(
sliderMax == 0 || sliderValue > sliderMax ? 0 : sliderValue / sliderMax,
position,
duration,
);
}

View File

@ -86,7 +86,6 @@ class SpotubeTrack extends Track {
.collection(BackendTrack.collection) .collection(BackendTrack.collection)
.getFirstListItem("spotify_id = '${track.id}'"), .getFirstListItem("spotify_id = '${track.id}'"),
).catchError((e, stack) { ).catchError((e, stack) {
Catcher.reportCheckedError(e, stack);
return null; return null;
}); });

View File

@ -58,6 +58,7 @@ class ArtistPage extends HookConsumerWidget {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
return SafeArea( return SafeArea(
bottom: false,
child: Scaffold( child: Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
leading: BackButton(), leading: BackButton(),

View File

@ -14,6 +14,7 @@ class LibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return const SafeArea( return const SafeArea(
bottom: false,
child: DefaultTabController( child: DefaultTabController(
length: 5, length: 5,
child: Scaffold( child: Scaffold(

View File

@ -62,8 +62,10 @@ class SearchPage extends HookConsumerWidget {
} }
return SafeArea( return SafeArea(
bottom: false,
child: Scaffold( child: Scaffold(
appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null, appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null,
extendBody: true,
body: !authenticationNotifier.isLoggedIn body: !authenticationNotifier.isLoggedIn
? const AnonymousFallback() ? const AnonymousFallback()
: Column( : Column(

View File

@ -41,9 +41,10 @@ class SettingsPage extends HookConsumerWidget {
}, [preferences.downloadLocation]); }, [preferences.downloadLocation]);
return SafeArea( return SafeArea(
bottom: false,
child: Scaffold( child: Scaffold(
appBar: PageWindowTitleBar( appBar: const PageWindowTitleBar(
title: const Text("Settings"), title: Text("Settings"),
centerTitle: true, centerTitle: true,
), ),
body: Row( body: Row(

View File

@ -190,7 +190,8 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
// skip all the activeTrack.skipSegments // skip all the activeTrack.skipSegments
if (state?.isLoading != true && if (state?.isLoading != true &&
(state?.activeTrack as SpotubeTrack).skipSegments.isNotEmpty && (state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty ==
true &&
preferences.skipSponsorSegments) { preferences.skipSponsorSegments) {
for (final segment for (final segment
in (state!.activeTrack as SpotubeTrack).skipSegments) { in (state!.activeTrack as SpotubeTrack).skipSegments) {

View File

@ -1393,6 +1393,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.3" version: "1.0.3"
simple_circular_progress_bar:
dependency: "direct main"
description:
name: simple_circular_progress_bar
sha256: e661ca942fbc617298e975b41fde19003d995de73ca6c2a1526c54d52f07151b
url: "https://pub.dev"
source: hosted
version: "1.0.2"
skeleton_text: skeleton_text:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -64,6 +64,7 @@ dependencies:
queue: ^3.1.0+1 queue: ^3.1.0+1
scroll_to_index: ^3.0.1 scroll_to_index: ^3.0.1
shared_preferences: ^2.0.11 shared_preferences: ^2.0.11
simple_circular_progress_bar: ^1.0.2
skeleton_text: ^3.0.0 skeleton_text: ^3.0.0
spotify: spotify:
git: git: