Skip to content

Commit

Permalink
feat: merge floating player with nav bar and nav bar translucent bg
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Mar 10, 2023
1 parent 67380f6 commit a90261e
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 155 deletions.
49 changes: 16 additions & 33 deletions lib/components/player/player_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/primitive_utils.dart';
Expand Down Expand Up @@ -58,28 +59,24 @@ class PlayerControls extends HookConsumerWidget {
children: [
HookBuilder(
builder: (context) {
final duration =
useStream(PlaylistQueueNotifier.duration).data ??
Duration.zero;
final positionSnapshot =
useStream(PlaylistQueueNotifier.position);
final position = positionSnapshot.data ?? Duration.zero;
final progressObj = useProgress(ref);

final progressStatic = progressObj.item1;
final position = progressObj.item2;
final duration = progressObj.item3;

final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60));
duration.inMinutes.remainder(60),
);
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60));
duration.inSeconds.remainder(60),
);
final currentMinutes = PrimitiveUtils.zeroPadNumStr(
position.inMinutes.remainder(60));
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;
position.inSeconds.remainder(60),
);

final progress = useState<num>(
useMemoized(() => progressStatic, []),
Expand All @@ -90,20 +87,6 @@ class PlayerControls extends HookConsumerWidget {
return null;
}, [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(
children: [
Tooltip(
Expand All @@ -121,7 +104,7 @@ class PlayerControls extends HookConsumerWidget {
onChangeEnd: (value) async {
await playlistNotifier.seek(
Duration(
seconds: (value * sliderMax).toInt(),
seconds: (value * duration.inSeconds).toInt(),
),
);
},
Expand Down
149 changes: 91 additions & 58 deletions lib/components/player/player_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:spotube/collections/spotube_icons.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/hooks/use_progress.dart';
import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/utils/service_utils.dart';

Expand All @@ -22,7 +22,6 @@ class PlayerOverlay extends HookConsumerWidget {

@override
Widget build(BuildContext context, ref) {
final paletteColor = usePaletteColor(albumArt, ref);
final canShow = ref.watch(
PlaylistQueueNotifier.provider.select((s) => s != null),
);
Expand All @@ -31,6 +30,13 @@ class PlayerOverlay extends HookConsumerWidget {
final playing = useStream(PlaylistQueueNotifier.playing).data ??
PlaylistQueueNotifier.isPlaying;

final textColor = Theme.of(context).colorScheme.primary;

const radius = BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
);

return GestureDetector(
onVerticalDragEnd: (details) {
int sensitivity = 8;
Expand All @@ -40,80 +46,107 @@ class PlayerOverlay extends HookConsumerWidget {
}
},
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
borderRadius: radius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: MediaQuery.of(context).size.width,
height: canShow ? 50 : 0,
height: canShow ? 53 : 0,
decoration: BoxDecoration(
color: paletteColor.color.withOpacity(.7),
border: Border.all(
color: paletteColor.titleTextColor,
width: 2,
),
borderRadius: BorderRadius.circular(5),
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(.8),
borderRadius: radius,
),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: canShow ? 1 : 0,
child: Material(
type: MaterialType.transparency,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => GoRouter.of(context).push("/player"),
child: PlayerTrackDetails(
albumArt: albumArt,
color: paletteColor.bodyTextColor,
),
),
),
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,
),
);
},
);
},
),
Row(
children: [
IconButton(
icon: Icon(
SpotubeIcons.skipBack,
color: paletteColor.bodyTextColor,
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,
),
),
),
),
onPressed: playlistNotifier.previous,
),
Consumer(
builder: (context, ref, _) {
return IconButton(
icon: playlist?.isLoading == true
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: Icon(
playing
? SpotubeIcons.pause
: SpotubeIcons.play,
color: paletteColor.bodyTextColor,
Row(
children: [
IconButton(
icon: Icon(
SpotubeIcons.skipBack,
color: textColor,
),
onPressed: playlistNotifier.previous,
),
Consumer(
builder: (context, ref, _) {
return IconButton(
icon: playlist?.isLoading == true
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: Icon(
playing
? SpotubeIcons.pause
: SpotubeIcons.play,
color: textColor,
),
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
);
},
),
);
},
),
IconButton(
icon: Icon(
SpotubeIcons.skipForward,
color: paletteColor.bodyTextColor,
IconButton(
icon: Icon(
SpotubeIcons.skipForward,
color: textColor,
),
onPressed: playlistNotifier.next,
),
],
),
onPressed: playlistNotifier.next,
),
],
],
),
),
],
),
Expand Down
62 changes: 42 additions & 20 deletions lib/components/player/player_track_details.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';

import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
Expand All @@ -20,38 +21,59 @@ class PlayerTrackDetails extends HookConsumerWidget {

return Row(
children: [
if (albumArt != null)
Padding(
padding: const EdgeInsets.all(5.0),
child: UniversalImage(
path: albumArt!,
height: 50,
width: 50,
placeholder: (context, url) {
return Assets.albumPlaceholder.image(
height: 50,
width: 50,
);
},
if (playback != null)
Container(
padding: const EdgeInsets.all(6),
constraints: const BoxConstraints(
maxWidth: 70,
maxHeight: 70,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
path: albumArt ?? "",
placeholder: (context, url) {
return Assets.albumPlaceholder.image(
height: 50,
width: 50,
);
},
),
),
),
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
Flexible(
child: Text(
playback?.activeTrack.name ?? "Not playing",
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
playback?.activeTrack.name ?? "",
overflow: TextOverflow.ellipsis,
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))
Flexible(
flex: 1,
child: Column(
children: [
Text(
playback?.activeTrack.name ?? "Not playing",
playback?.activeTrack.name ?? "",
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
),
Expand Down
5 changes: 1 addition & 4 deletions lib/components/root/bottom_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ class BottomPlayer extends HookConsumerWidget {
if (layoutMode == LayoutMode.compact ||
(breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
layoutMode == LayoutMode.adaptive)) {
return Padding(
padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8, top: 0),
child: PlayerOverlay(albumArt: albumArt),
);
return PlayerOverlay(albumArt: albumArt);
}

return DecoratedBox(
Expand Down

0 comments on commit a90261e

Please sign in to comment.