diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 73e0fc9f..ebb42325 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -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'; @@ -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( useMemoized(() => progressStatic, []), @@ -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( @@ -121,7 +104,7 @@ class PlayerControls extends HookConsumerWidget { onChangeEnd: (value) async { await playlistNotifier.seek( Duration( - seconds: (value * sliderMax).toInt(), + seconds: (value * duration.inSeconds).toInt(), ), ); }, diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index 6877362e..8a4cbb76 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -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'; @@ -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), ); @@ -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; @@ -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, - ), - ), - ), - ), - Row( - children: [ - IconButton( - icon: Icon( - SpotubeIcons.skipBack, - color: paletteColor.bodyTextColor, - ), - 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, - ), - onPressed: Actions.handler( - context, - PlayPauseIntent(ref), + HookBuilder( + builder: (context) { + final progress = useProgress(ref); + // animated + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 250), + tween: Tween(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, ), ); }, - ), - IconButton( - icon: Icon( - SpotubeIcons.skipForward, - 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.next, - ), - ], + 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( + context, + PlayPauseIntent(ref), + ), + ); + }, + ), + IconButton( + icon: Icon( + SpotubeIcons.skipForward, + color: textColor, + ), + onPressed: playlistNotifier.next, + ), + ], + ), + ], + ), ), ], ), diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index f8878443..5ecc11f0 100644 --- a/lib/components/player/player_track_details.dart +++ b/lib/components/player/player_track_details.dart @@ -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'; @@ -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( + 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), ), diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 226dd09f..851e37da 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -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( diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index 9bcbd189..8469c864 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:curved_navigation_bar/curved_navigation_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -45,44 +47,52 @@ class SpotubeNavigationBar extends HookConsumerWidget { (breakpoint.isMoreThan(Breakpoints.sm) && layoutMode == LayoutMode.adaptive)) return const SizedBox(); - return CurvedNavigationBar( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - buttonBackgroundColor: buttonColor, - color: Theme.of(context).colorScheme.background, - height: 50, - items: [ - ...navbarTileList.map( - (e) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: Badge( - backgroundColor: Theme.of(context).primaryColor, - isLabelVisible: e.title == "Library" && downloadCount > 0, - label: Text( - downloadCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 10, + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: CurvedNavigationBar( + backgroundColor: Theme.of(context) + .colorScheme + .secondaryContainer + .withOpacity(0.72), + buttonBackgroundColor: buttonColor, + color: Theme.of(context).colorScheme.background, + height: 50, + items: [ + ...navbarTileList.map( + (e) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: Badge( + backgroundColor: Theme.of(context).primaryColor, + isLabelVisible: e.title == "Library" && downloadCount > 0, + label: Text( + downloadCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + child: Icon( + e.icon, + color: Theme.of(context).colorScheme.primary, + ), ), - ), - child: Icon( - e.icon, - color: Theme.of(context).colorScheme.primary, - ), - ), - ); + ); + }, + ), + ], + index: insideSelectedIndex.value, + onTap: (i) { + insideSelectedIndex.value = i; + if (navbarTileList[i].title == "Settings") { + Sidebar.goToSettings(context); + return; + } + onSelectedIndexChanged(i); }, ), - ], - index: insideSelectedIndex.value, - onTap: (i) { - insideSelectedIndex.value = i; - if (navbarTileList[i].title == "Settings") { - Sidebar.goToSettings(context); - return; - } - onSelectedIndexChanged(i); - }, + ), ); } } diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index 718bde50..b6e76c1a 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -180,6 +180,7 @@ class TrackCollectionView extends HookConsumerWidget { ); return SafeArea( + bottom: false, child: Scaffold( appBar: kIsDesktop ? PageWindowTitleBar( diff --git a/lib/hooks/use_progress.dart b/lib/hooks/use_progress.dart new file mode 100644 index 00000000..25938de8 --- /dev/null +++ b/lib/hooks/use_progress.dart @@ -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 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, + ); +} diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 95d2998d..33e9fe45 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -86,7 +86,6 @@ class SpotubeTrack extends Track { .collection(BackendTrack.collection) .getFirstListItem("spotify_id = '${track.id}'"), ).catchError((e, stack) { - Catcher.reportCheckedError(e, stack); return null; }); diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index d8e07abe..5be33a4b 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -58,6 +58,7 @@ class ArtistPage extends HookConsumerWidget { final auth = ref.watch(AuthenticationNotifier.provider); return SafeArea( + bottom: false, child: Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index 04b0bf73..e809f039 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -14,6 +14,7 @@ class LibraryPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { return const SafeArea( + bottom: false, child: DefaultTabController( length: 5, child: Scaffold( diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 1209add6..fe339151 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -62,8 +62,10 @@ class SearchPage extends HookConsumerWidget { } return SafeArea( + bottom: false, child: Scaffold( - appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null, + appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null, + extendBody: true, body: !authenticationNotifier.isLoggedIn ? const AnonymousFallback() : Column( diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index bc41a915..8a1a4236 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -41,9 +41,10 @@ class SettingsPage extends HookConsumerWidget { }, [preferences.downloadLocation]); return SafeArea( + bottom: false, child: Scaffold( - appBar: PageWindowTitleBar( - title: const Text("Settings"), + appBar: const PageWindowTitleBar( + title: Text("Settings"), centerTitle: true, ), body: Row( diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index f63ff6d6..324a4138 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -190,7 +190,8 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { // skip all the activeTrack.skipSegments if (state?.isLoading != true && - (state?.activeTrack as SpotubeTrack).skipSegments.isNotEmpty && + (state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty == + true && preferences.skipSponsorSegments) { for (final segment in (state!.activeTrack as SpotubeTrack).skipSegments) { diff --git a/pubspec.lock b/pubspec.lock index 7391050f..77f779f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1393,6 +1393,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7e2730e8..e68bc0d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: queue: ^3.1.0+1 scroll_to_index: ^3.0.1 shared_preferences: ^2.0.11 + simple_circular_progress_bar: ^1.0.2 skeleton_text: ^3.0.0 spotify: git: