From da2e371dfdaee09a9d8e89e57220f28a9ce13587 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 5 Jul 2022 14:08:56 +0600 Subject: [PATCH] Blur effect applied in Lyrics, PlayerView & PlayerOverlay Drag to scroll support for Playlist Categories Sidebar user name & settings icon moved down --- lib/components/Category/CategoryCard.dart | 53 +++--- lib/components/Home/Home.dart | 31 +-- lib/components/Home/Sidebar.dart | 169 ++++++++++------- lib/components/Lyrics/Lyrics.dart | 98 +++++----- lib/components/Lyrics/SyncedLyrics.dart | 176 +++++++++++------- lib/components/Player/PlayerActions.dart | 7 +- lib/components/Player/PlayerControls.dart | 1 + lib/components/Player/PlayerOverlay.dart | 100 +++++----- lib/components/Player/PlayerQueue.dart | 104 ++++++++--- lib/components/Player/PlayerView.dart | 137 ++++++++------ lib/components/Shared/PageWindowTitleBar.dart | 90 +++++---- lib/hooks/useCustomStatusBarColor.dart | 9 +- lib/hooks/usePaletteColor.dart | 4 +- 13 files changed, 595 insertions(+), 384 deletions(-) diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 88f833b3..c5eb9ccf 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -65,30 +66,40 @@ class CategoryCard extends HookConsumerWidget { ? const Text("Something Went Wrong") : SizedBox( height: 245, - child: Scrollbar( - controller: scrollController, - child: PagedListView( - shrinkWrap: true, - pagingController: pagingController, - scrollController: scrollController, - scrollDirection: Axis.horizontal, - builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (context) { - return const NotFound(); - }, - firstPageProgressIndicatorBuilder: (context) { - return const ShimmerPlaybuttonCard(); - }, - newPageProgressIndicatorBuilder: (context) { - return const ShimmerPlaybuttonCard(); - }, - itemBuilder: (context, playlist, index) { - return PlaylistCard(playlist); - }, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: scrollController, + interactive: false, + child: PagedListView( + shrinkWrap: true, + pagingController: pagingController, + scrollController: scrollController, + scrollDirection: Axis.horizontal, + builderDelegate: + PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (context) { + return const NotFound(); + }, + firstPageProgressIndicatorBuilder: (context) { + return const ShimmerPlaybuttonCard(); + }, + newPageProgressIndicatorBuilder: (context) { + return const ShimmerPlaybuttonCard(); + }, + itemBuilder: (context, playlist, index) { + return PlaylistCard(playlist); + }, + ), ), ), ), - ) + ), ], ); } diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index b6433417..53bb1bb4 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -57,11 +57,11 @@ class Home extends HookConsumerWidget { useUpdateChecker(ref); final titleBarContents = Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Row( - children: [ - Expanded( - child: Row( + color: Theme.of(context).scaffoldBackgroundColor, + child: Row( + children: [ + Expanded( + child: Row( children: [ Container( constraints: BoxConstraints( @@ -74,9 +74,11 @@ class Home extends HookConsumerWidget { if (!Platform.isMacOS && !kIsMobile) const TitleBarActionButtons(), ], - )) - ], - )); + ), + ) + ], + ), + ); final backgroundColor = Theme.of(context).backgroundColor; @@ -96,9 +98,10 @@ class Home extends HookConsumerWidget { child: Scaffold( body: Column( children: [ - kIsMobile - ? titleBarContents - : WindowTitleBarBox(child: titleBarContents), + if (_selectedIndex.value != 3) + kIsMobile + ? titleBarContents + : WindowTitleBarBox(child: titleBarContents), Expanded( child: Row( children: [ @@ -110,7 +113,11 @@ class Home extends HookConsumerWidget { if (_selectedIndex.value == 0) Expanded( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only( + bottom: 8.0, + top: 8.0, + left: 8.0, + ), child: HookBuilder(builder: (context) { final pagingController = usePaginatedFutureProvider< Page, int, Category>( diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index e0593dce..546ab79e 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -1,9 +1,11 @@ +import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -37,6 +39,14 @@ class Sidebar extends HookConsumerWidget { final extended = useState(false); final meSnapshot = ref.watch(currentUserQuery); + final int titleBarDragMaxWidth = useBreakpointValue( + md: 80, + lg: 256, + sm: 0, + xl: 0, + xxl: 0, + ); + useEffect(() { if (breakpoints.isMd && extended.value) { extended.value = false; @@ -47,75 +57,100 @@ class Sidebar extends HookConsumerWidget { return null; }); - return NavigationRail( - destinations: sidebarTileList - .map( - (e) => NavigationRailDestination( - icon: Icon(e.icon), - label: Text( - e.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + return Material( + color: Theme.of(context).navigationRailTheme.backgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (selectedIndex == 3) + SizedBox( + height: appWindow.titleBarHeight, + width: titleBarDragMaxWidth.toDouble(), + child: MoveWindow(), + ), + extended.value + ? Padding( + padding: const EdgeInsets.only(left: 15), + child: Row(children: [ + _buildSmallLogo(), + const SizedBox( + width: 10, + ), + Text("Spotube", + style: Theme.of(context).textTheme.headline4), + ]), + ) + : _buildSmallLogo(), + Expanded( + child: NavigationRail( + destinations: sidebarTileList + .map( + (e) => NavigationRailDestination( + icon: Icon(e.icon), + label: Text( + e.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ) + .toList(), + selectedIndex: selectedIndex, + onDestinationSelected: onSelectedIndexChanged, + extended: extended.value, + ), + ), + SizedBox( + width: titleBarDragMaxWidth.toDouble(), + child: meSnapshot.when( + data: (data) { + final avatarImg = imageToUrlString(data.images, + index: (data.images?.length ?? 1) - 1); + if (extended.value) { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CircleAvatar( + backgroundImage: + CachedNetworkImageProvider(avatarImg), + ), + const SizedBox(width: 10), + Text( + data.displayName ?? "Guest", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: () => goToSettings(context)), + ], + )); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () => goToSettings(context), + child: CircleAvatar( + backgroundImage: CachedNetworkImageProvider(avatarImg), + ), + ), + ); + } + }, + error: (e, _) => Text("Error $e"), + loading: () => const CircularProgressIndicator(), ), ) - .toList(), - selectedIndex: selectedIndex, - onDestinationSelected: onSelectedIndexChanged, - extended: extended.value, - leading: extended.value - ? Padding( - padding: const EdgeInsets.only(left: 15), - child: Row(children: [ - _buildSmallLogo(), - const SizedBox( - width: 10, - ), - Text("Spotube", style: Theme.of(context).textTheme.headline4), - ]), - ) - : _buildSmallLogo(), - trailing: meSnapshot.when( - data: (data) { - final avatarImg = imageToUrlString(data.images, - index: (data.images?.length ?? 1) - 1); - return extended.value - ? Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar( - backgroundImage: - CachedNetworkImageProvider(avatarImg), - ), - const SizedBox(width: 10), - Text( - data.displayName ?? "Guest", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.settings_outlined), - onPressed: () => goToSettings(context)), - ], - )) - : InkWell( - onTap: () => goToSettings(context), - child: CircleAvatar( - backgroundImage: CachedNetworkImageProvider(avatarImg), - ), - ); - }, - error: (e, _) => Text("Error $e"), - loading: () => const CircularProgressIndicator(), + ], ), ); } diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index ce3687ef..967fc7be 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -2,13 +2,18 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; class Lyrics extends HookConsumerWidget { - const Lyrics({Key? key}) : super(key: key); + final Color? titleBarForegroundColor; + const Lyrics({ + required this.titleBarForegroundColor, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context, ref) { @@ -17,58 +22,57 @@ class Lyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final textTheme = Theme.of(context).textTheme; - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - playback.track?.name ?? "", - style: breakpoint >= Breakpoints.md - ? textTheme.headline3 - : textTheme.headline4?.copyWith(fontSize: 25), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + PageWindowTitleBar(foregroundColor: titleBarForegroundColor), + Center( + child: Text( + playback.track?.name ?? "", + style: breakpoint >= Breakpoints.md + ? textTheme.headline3 + : textTheme.headline4?.copyWith(fontSize: 25), ), - Center( - child: Text( - artistsToString(playback.track?.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6, - ), + ), + Center( + child: Text( + artistsToString(playback.track?.artists ?? []), + style: breakpoint >= Breakpoints.md + ? textTheme.headline5 + : textTheme.headline6, ), - Expanded( - child: SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: geniusLyricsSnapshot.when( - data: (lyrics) { - return Text( - lyrics == null && playback.track == null - ? "No Track being played currently" - : lyrics!, - style: textTheme.headline6 - ?.copyWith(color: textTheme.headline1?.color), - ); - }, - error: (error, __) => Text( - "Sorry, no Lyrics were found for `${playback.track?.name}` :'("), - loading: () => const ShimmerLyrics(), - ), + ), + Expanded( + child: SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: geniusLyricsSnapshot.when( + data: (lyrics) { + return Text( + lyrics == null && playback.track == null + ? "No Track being played currently" + : lyrics!, + style: textTheme.headline6 + ?.copyWith(color: textTheme.headline1?.color), + ); + }, + error: (error, __) => Text( + "Sorry, no Lyrics were found for `${playback.track?.name}` :'("), + loading: () => const ShimmerLyrics(), ), ), ), ), - const Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text("Powered by genius.com"), - ), - ) - ], - ), + ), + const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text("Powered by genius.com"), + ), + ) + ], ); } } diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 92f1be61..6b1e9a55 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -1,13 +1,20 @@ +import 'dart:ui'; + +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/helpers/artist-to-string.dart'; +import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -88,79 +95,116 @@ class SyncedLyrics extends HookConsumerWidget { }, [lyricValue]); // when synced lyrics not found, fallback to GeniusLyrics - if (failed.value) return const Lyrics(); - final headlineTextStyle = breakpoint >= Breakpoints.md - ? textTheme.headline3 - : textTheme.headline4?.copyWith(fontSize: 25); + String albumArt = useMemoized( + () => imageToUrlString( + playback.track?.album?.images, + index: (playback.track?.album?.images?.length ?? 1) - 1, + ), + [playback.track?.album?.images], + ); + final palette = usePaletteColor(albumArt, ref); + + final headlineTextStyle = (breakpoint >= Breakpoints.md + ? textTheme.headline3 + : textTheme.headline4?.copyWith(fontSize: 25)) + ?.copyWith(color: palette.titleTextColor); + return Expanded( - child: Column( - children: [ - Center( - child: SizedBox( - height: breakpoint >= Breakpoints.md ? 50 : 30, - child: playback.track?.name != null && - playback.track!.name!.length > 29 - ? SpotubeMarqueeText( - text: playback.track?.name ?? "Not Playing", - style: headlineTextStyle, - ) - : Text( - playback.track?.name ?? "Not Playing", - style: headlineTextStyle, - ), - )), - Center( - child: Text( - artistsToString(playback.track?.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + image: DecorationImage( + image: CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, ), + fit: BoxFit.cover, ), - if (lyricValue != null && lyricValue.lyrics.isNotEmpty) - Expanded( - child: ListView.builder( - controller: controller, - itemCount: lyricValue.lyrics.length, - itemBuilder: (context, index) { - final lyricSlice = lyricValue.lyrics[index]; - final isActive = lyricSlice.time.inSeconds == currentTime; - if (isActive) { - controller.scrollToIndex( - index, - preferPosition: AutoScrollPosition.middle, - ); - } - return AutoScrollTag( - key: ValueKey(index), - index: index, - controller: controller, - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + color: palette.color.withOpacity(.7), + child: failed.value + ? Lyrics(titleBarForegroundColor: palette.bodyTextColor) + : Column( + children: [ + PageWindowTitleBar( + foregroundColor: palette.bodyTextColor, + ), + Center( + child: SizedBox( + height: breakpoint >= Breakpoints.md ? 50 : 30, + child: playback.track?.name != null && + playback.track!.name!.length > 29 + ? SpotubeMarqueeText( + text: playback.track?.name ?? "Not Playing", + style: headlineTextStyle, + ) + : Text( + playback.track?.name ?? "Not Playing", + style: headlineTextStyle, + ), + )), + Center( child: Text( - lyricSlice.text, - style: TextStyle( - // indicating the active state of that lyric slice - color: isActive - ? Theme.of(context).primaryColor - : null, - fontWeight: isActive ? FontWeight.bold : null, - fontSize: 30, - ), - textAlign: TextAlign.center, + artistsToString( + playback.track?.artists ?? []), + style: breakpoint >= Breakpoints.md + ? textTheme.headline5 + : textTheme.headline6, ), ), - ), - ); - }, - ), - ), - if (playback.track != null && - (lyricValue == null || lyricValue.lyrics.isEmpty == true)) - const Expanded(child: ShimmerLyrics()), - ], + if (lyricValue != null && lyricValue.lyrics.isNotEmpty) + Expanded( + child: ListView.builder( + controller: controller, + itemCount: lyricValue.lyrics.length, + itemBuilder: (context, index) { + final lyricSlice = lyricValue.lyrics[index]; + final isActive = + lyricSlice.time.inSeconds == currentTime; + if (isActive) { + controller.scrollToIndex( + index, + preferPosition: AutoScrollPosition.middle, + ); + } + return AutoScrollTag( + key: ValueKey(index), + index: index, + controller: controller, + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + lyricSlice.text, + style: TextStyle( + // indicating the active state of that lyric slice + color: isActive + ? Theme.of(context).primaryColor + : palette.bodyTextColor, + fontWeight: + isActive ? FontWeight.bold : null, + fontSize: 30, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + }, + ), + ), + if (playback.track != null && + (lyricValue == null || + lyricValue.lyrics.isEmpty == true)) + const Expanded(child: ShimmerLyrics()), + ], + ), + ), + ), ), ); } diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart index dec78928..69f0797b 100644 --- a/lib/components/Player/PlayerActions.dart +++ b/lib/components/Player/PlayerActions.dart @@ -13,8 +13,10 @@ import 'package:spotube/provider/SpotifyRequests.dart'; class PlayerActions extends HookConsumerWidget { final MainAxisAlignment mainAxisAlignment; + final bool floatingQueue; PlayerActions({ this.mainAxisAlignment = MainAxisAlignment.center, + this.floatingQueue = true, Key? key, }) : super(key: key); final logger = getLogger(PlayerActions); @@ -37,7 +39,8 @@ class PlayerActions extends HookConsumerWidget { isDismissible: true, enableDrag: true, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Colors.black12, + barrierColor: Colors.black12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -45,7 +48,7 @@ class PlayerActions extends HookConsumerWidget { maxHeight: MediaQuery.of(context).size.height * .7, ), builder: (context) { - return const PlayerQueue(); + return PlayerQueue(floating: floatingQueue); }, ); } diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 33f41b1f..17f28da6 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -8,6 +8,7 @@ import 'package:spotube/provider/Playback.dart'; class PlayerControls extends HookConsumerWidget { final Color? iconColor; + PlayerControls({ this.iconColor, Key? key, diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index 6526b602..958874c9 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -20,7 +22,7 @@ class PlayerOverlay extends HookConsumerWidget { Widget build(BuildContext context, ref) { final breakpoint = useBreakpoints(); final isCurrentRoute = useIsCurrentRoute("/"); - final paletteColor = usePaletteColor(context, albumArt, ref); + final paletteColor = usePaletteColor(albumArt, ref); final playback = ref.watch(playbackProvider); if (isCurrentRoute == false) { @@ -45,56 +47,66 @@ class PlayerOverlay extends HookConsumerWidget { GoRouter.of(context).push("/player"); } }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - width: MediaQuery.of(context).size.width, - height: 50, - decoration: BoxDecoration( - color: paletteColor.color, - borderRadius: BorderRadius.circular(5), - ), - child: Material( - type: MaterialType.transparency, - 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: paletteColor.bodyTextColor, - ), - ), - ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + width: MediaQuery.of(context).size.width, + height: 50, + decoration: BoxDecoration( + color: paletteColor.color.withOpacity(.7), + border: Border.all( + color: paletteColor.titleTextColor, + width: 2, ), - Row( + borderRadius: BorderRadius.circular(5), + ), + child: Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( - icon: const Icon(Icons.skip_previous_rounded), - color: paletteColor.bodyTextColor, - onPressed: () { - onPrevious(); - }), - IconButton( - icon: Icon( - playback.isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => GoRouter.of(context).push("/player"), + child: PlayerTrackDetails( + albumArt: albumArt, + color: paletteColor.bodyTextColor, + ), + ), ), - color: paletteColor.bodyTextColor, - onPressed: _playOrPause, ), - IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext(), - color: paletteColor.bodyTextColor, + Row( + children: [ + IconButton( + icon: const Icon(Icons.skip_previous_rounded), + color: paletteColor.bodyTextColor, + onPressed: () { + onPrevious(); + }), + IconButton( + icon: Icon( + playback.isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + color: paletteColor.bodyTextColor, + onPressed: _playOrPause, + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext(), + color: paletteColor.bodyTextColor, + ), + ], ), ], ), - ], + ), ), ), ), diff --git a/lib/components/Player/PlayerQueue.dart b/lib/components/Player/PlayerQueue.dart index 4f0b35a4..bbac3229 100644 --- a/lib/components/Player/PlayerQueue.dart +++ b/lib/components/Player/PlayerQueue.dart @@ -1,73 +1,119 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/components/Shared/NotFound.dart'; import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/provider/Playback.dart'; -import 'package:collection/collection.dart'; class PlayerQueue extends HookConsumerWidget { - const PlayerQueue({Key? key}) : super(key: key); + final bool floating; + const PlayerQueue({ + this.floating = true, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context, ref) { final playback = ref.watch(playbackProvider); + final controller = useAutoScrollController(); final tracks = playback.playlist?.tracks ?? []; if (tracks.isEmpty) { return const NotFound(vertical: true); } + final borderRadius = floating + ? BorderRadius.circular(10) + : const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ); + final headlineColor = Theme.of(context).textTheme.headline4?.color; + + useEffect(() { + if (playback.track == null || playback.playlist == null) return null; + final index = playback.playlist!.tracks + .indexWhere((track) => track.id == playback.track!.id); + if (index < 0) return; + controller.scrollToIndex( + index, + preferPosition: AutoScrollPosition.middle, + ); + return null; + }, []); + return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + filter: ImageFilter.blur( + sigmaX: 12.0, + sigmaY: 12.0, + ), child: Container( + margin: EdgeInsets.all(floating ? 8.0 : 0), + padding: const EdgeInsets.only( + top: 5.0, + ), decoration: BoxDecoration( color: Theme.of(context) .navigationRailTheme .backgroundColor ?.withOpacity(0.5), - borderRadius: BorderRadius.circular(10), - ), - margin: const EdgeInsets.all(8.0), - padding: const EdgeInsets.only( - top: 5.0, + borderRadius: borderRadius, ), child: Column( children: [ + Container( + height: 5, + width: 100, + margin: const EdgeInsets.only(bottom: 5, top: 2), + decoration: BoxDecoration( + color: headlineColor, + borderRadius: BorderRadius.circular(20), + ), + ), Text( "Queue (${playback.playlist?.name})", - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headline4?.copyWith( + fontWeight: FontWeight.bold, + ), overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 10), Flexible( - child: ListView( - shrinkWrap: true, - children: [ - ...tracks.asMap().entries.mapIndexed((i, track) { + child: ListView.builder( + controller: controller, + itemCount: tracks.length, + shrinkWrap: true, + itemBuilder: (context, i) { + final track = tracks.asMap().entries.elementAt(i); String duration = "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: TrackTile( - playback, - track: track, - duration: duration, - thumbnailUrl: - imageToUrlString(track.value.album?.images), - isActive: playback.track?.id == track.value.id, - onTrackPlayButtonPressed: (currentTrack) async { - if (playback.track?.id == track.value.id) return; - await playback.setPlaylistPosition(i); - }, + return AutoScrollTag( + key: ValueKey(i), + controller: controller, + index: i, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( + playback, + track: track, + duration: duration, + thumbnailUrl: + imageToUrlString(track.value.album?.images), + isActive: playback.track?.id == track.value.id, + onTrackPlayButtonPressed: (currentTrack) async { + if (playback.track?.id == track.value.id) return; + await playback.setPlaylistPosition(i); + }, + ), ), ); }), - ], - ), ), ], ), diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index 63c2b074..6836f057 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -44,78 +46,101 @@ class PlayerView extends HookConsumerWidget { [currentTrack?.album?.images], ); - final PaletteColor paletteColor = usePaletteColor(context, albumArt, ref); + final PaletteColor paletteColor = usePaletteColor(albumArt, ref); useCustomStatusBarColor( paletteColor.color, GoRouter.of(context).location == "/player", + noSetBGColor: true, ); return SafeArea( child: Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), - backgroundColor: Colors.transparent, - ), - backgroundColor: paletteColor.color, - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), + // backgroundColor: paletteColor.color, + body: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + ), + fit: BoxFit.cover, + ), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + color: paletteColor.color.withOpacity(.5), child: Column( children: [ - SizedBox( - height: 30, - child: currentTrack?.name != null && - currentTrack!.name!.length > 29 - ? SpotubeMarqueeText( - text: currentTrack.name ?? "Not playing", - style: - Theme.of(context).textTheme.headline5?.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.titleTextColor, - ), - ) - : Text( - currentTrack?.name ?? "Not Playing", - style: Theme.of(context).textTheme.headline5, - ), + PageWindowTitleBar( + leading: const BackButton(), + backgroundColor: Colors.transparent, + foregroundColor: paletteColor.titleTextColor, ), - artistsToClickableArtists( - currentTrack?.artists ?? [], - textStyle: Theme.of(context).textTheme.headline6!.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.bodyTextColor, + Padding( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + SizedBox( + height: 30, + child: currentTrack?.name != null && + currentTrack!.name!.length > 29 + ? SpotubeMarqueeText( + text: currentTrack.name ?? "Not playing", + style: Theme.of(context) + .textTheme + .headline5 + ?.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.titleTextColor, + ), + ) + : Text( + currentTrack?.name ?? "Not Playing", + style: Theme.of(context).textTheme.headline5, + ), ), + artistsToClickableArtists( + currentTrack?.artists ?? [], + textStyle: + Theme.of(context).textTheme.headline6!.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.bodyTextColor, + ), + ), + ], + ), ), + const Spacer(), + HookBuilder(builder: (context) { + final ticker = useSingleTickerProvider(); + final controller = useAnimationController( + duration: const Duration(seconds: 10), + vsync: ticker, + )..repeat(); + return RotationTransition( + turns: Tween(begin: 0.0, end: 1.0).animate(controller), + child: CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + ), + radius: MediaQuery.of(context).size.width * + (breakpoint.isSm ? 0.4 : 0.3), + ), + ); + }), + const Spacer(), + PlayerActions( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + floatingQueue: false, + ), + PlayerControls(iconColor: paletteColor.bodyTextColor), ], ), ), - const Spacer(), - HookBuilder(builder: (context) { - final ticker = useSingleTickerProvider(); - final controller = useAnimationController( - duration: const Duration(seconds: 10), - vsync: ticker, - )..repeat(); - return RotationTransition( - turns: Tween(begin: 0.0, end: 1.0).animate(controller), - child: CircleAvatar( - backgroundImage: CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - ), - radius: MediaQuery.of(context).size.width * - (breakpoint.isSm ? 0.4 : 0.3), - ), - ); - }), - const Spacer(), - PlayerActions( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - ), - PlayerControls(iconColor: paletteColor.bodyTextColor), - ], + ), ), ), ); diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index 01611613..4579c4ed 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -13,42 +13,60 @@ class TitleBarActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - TextButton( - onPressed: () { - appWindow.minimize(); - }, - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Theme.of(context).iconTheme.color), - ), - child: Icon( - Icons.minimize_rounded, - color: color, - )), - TextButton( - onPressed: () async { - appWindow.maximizeOrRestore(); - }, - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Theme.of(context).iconTheme.color), - ), - child: Icon(Icons.crop_square_rounded, color: color)), - TextButton( - onPressed: () { - appWindow.close(); - }, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - color ?? Theme.of(context).iconTheme.color), - overlayColor: MaterialStateProperty.all(Colors.redAccent), - ), - child: const Icon( - Icons.close_rounded, - )), - ], + return TextButtonTheme( + data: TextButtonThemeData( + style: ButtonStyle( + splashFactory: NoSplash.splashFactory, + shape: MaterialStateProperty.all(const RoundedRectangleBorder()), + overlayColor: MaterialStateProperty.all(Colors.black12), + padding: MaterialStateProperty.all(EdgeInsets.zero), + minimumSize: MaterialStateProperty.all(const Size(50, 40)), + maximumSize: MaterialStateProperty.all(const Size(50, 40)), + ), + ), + child: IconTheme( + data: const IconThemeData(size: 16), + child: Row( + children: [ + TextButton( + onPressed: () { + appWindow.minimize(); + }, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Theme.of(context).iconTheme.color), + ), + child: Icon( + Icons.minimize_rounded, + color: color, + )), + TextButton( + onPressed: () async { + appWindow.maximizeOrRestore(); + }, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Theme.of(context).iconTheme.color), + ), + child: Icon( + Icons.crop_square_rounded, + color: color, + )), + TextButton( + onPressed: () { + appWindow.close(); + }, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + color ?? Theme.of(context).iconTheme.color), + overlayColor: MaterialStateProperty.all(Colors.redAccent), + ), + child: const Icon( + Icons.close_rounded, + )), + ], + ), + ), ); } } diff --git a/lib/hooks/useCustomStatusBarColor.dart b/lib/hooks/useCustomStatusBarColor.dart index da2e5c47..79fba905 100644 --- a/lib/hooks/useCustomStatusBarColor.dart +++ b/lib/hooks/useCustomStatusBarColor.dart @@ -2,12 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -void useCustomStatusBarColor(Color color, bool isCurrentRoute) { +void useCustomStatusBarColor( + Color color, + bool isCurrentRoute, { + bool noSetBGColor = false, +}) { final context = useContext(); final backgroundColor = Theme.of(context).backgroundColor; resetStatusbar() => SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( - statusBarColor: backgroundColor, // status bar color + statusBarColor: + !noSetBGColor ? backgroundColor : null, // status bar color statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179 ? Brightness.dark : Brightness.light, diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart index c1577bc1..e5a472f5 100644 --- a/lib/hooks/usePaletteColor.dart +++ b/lib/hooks/usePaletteColor.dart @@ -10,8 +10,8 @@ final _paletteColorState = StateProvider( }, ); -PaletteColor usePaletteColor( - BuildContext context, String imageUrl, WidgetRef ref) { +PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) { + final context = useContext(); final paletteColor = ref.watch(_paletteColorState); final mounted = useIsMounted();