diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index cfe213df1..c67f5eafa 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -50,7 +50,11 @@ class UserAlbums extends HookConsumerWidget { return const AnonymousFallback(); } if (albumsQuery.isLoading || !albumsQuery.hasData) { - return const Center(child: ShimmerPlaybuttonCard(count: 7)); + return Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16.0), + child: const ShimmerPlaybuttonCard(count: 7), + ); } return RefreshIndicator( @@ -63,6 +67,7 @@ class UserAlbums extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: SafeArea( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( onChanged: (value) => searchText.value = value, diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 046945228..517a7e2d2 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -7,6 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/utils/platform.dart'; class PlaybuttonCard extends HookWidget { final void Function()? onTap; @@ -34,52 +35,70 @@ class PlaybuttonCard extends HookWidget { @override Widget build(BuildContext context) { + final textsKey = useMemoized(() => GlobalKey(), []); final theme = Theme.of(context); final radius = BorderRadius.circular(15); final double size = useBreakpointValue( - sm: 130, - md: 150, - others: 170, - ); + sm: 130, + md: 150, + others: 170, + ) ?? + 170; final end = useBreakpointValue( - sm: 5, - md: 7, - others: 10, + sm: 15, + others: 20, + ) ?? + 20; + + final textsHeight = useState( + (textsKey.currentContext?.findRenderObject() as RenderBox?) + ?.size + .height ?? + 110.00, ); - return Container( - constraints: BoxConstraints(maxWidth: size), - margin: margin, - child: Material( - color: Color.lerp( - theme.colorScheme.surfaceVariant, - theme.colorScheme.surface, - useBrightnessValue(.9, .7), - ), - borderRadius: radius, - shadowColor: theme.colorScheme.background, - elevation: 3, - child: InkWell( - mouseCursor: SystemMouseCursors.click, - onTap: onTap, - borderRadius: radius, - splashFactory: theme.splashFactory, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - clipBehavior: Clip.none, + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + textsHeight.value = + (textsKey.currentContext?.findRenderObject() as RenderBox?) + ?.size + .height ?? + textsHeight.value; + }); + return null; + }, [textsKey]); + + return Stack( + children: [ + Container( + constraints: BoxConstraints(maxWidth: size), + margin: margin, + child: Material( + color: Color.lerp( + theme.colorScheme.surfaceVariant, + theme.colorScheme.surface, + useBrightnessValue(.9, .7), + ), + borderRadius: radius, + shadowColor: theme.colorScheme.background, + elevation: 3, + child: InkWell( + mouseCursor: SystemMouseCursors.click, + onTap: onTap, + borderRadius: radius, + splashFactory: theme.splashFactory, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - margin: const EdgeInsets.only( + Padding( + padding: const EdgeInsets.only( left: 8, right: 8, top: 8, ), - constraints: BoxConstraints(maxHeight: size), child: ClipRRect( borderRadius: radius, child: UniversalImage( @@ -88,77 +107,79 @@ class PlaybuttonCard extends HookWidget { ), ), ), - Positioned.directional( - textDirection: TextDirection.ltr, - end: end, - bottom: -size * .15, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isPlaying) - IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.background, - foregroundColor: theme.colorScheme.primary, - minimumSize: const Size.square(10), + Column( + key: textsKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: AutoSizeText( + title, + maxLines: 1, + minFontSize: theme.textTheme.bodyMedium!.fontSize!, + overflow: TextOverflow.ellipsis, + ), + ), + if (description != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: AutoSizeText( + description!, + maxLines: 2, + style: theme.textTheme.bodySmall?.copyWith( + color: + theme.colorScheme.onSurface.withOpacity(.5), ), - icon: const Icon(SpotubeIcons.queueAdd), - onPressed: isLoading ? null : onAddToQueuePressed, + overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 5), - IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.primaryContainer, - foregroundColor: theme.colorScheme.primary, - minimumSize: const Size.square(10), - ), - icon: isLoading - ? SizedBox.fromSize( - size: const Size.square(15), - child: const CircularProgressIndicator( - strokeWidth: 2), - ) - : isPlaying - ? const Icon(SpotubeIcons.pause) - : const Icon(SpotubeIcons.play), - onPressed: isLoading ? null : onPlaybuttonPressed, ), - ], - ), + const SizedBox(height: 10), + ], ), ], ), - const SizedBox(height: 15), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: AutoSizeText( - title, - maxLines: 1, - minFontSize: theme.textTheme.bodyMedium!.fontSize!, - overflow: TextOverflow.ellipsis, + ), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + right: end, + bottom: textsHeight.value - (kIsMobile ? 5 : 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isPlaying) + IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.background, + foregroundColor: theme.colorScheme.primary, + minimumSize: const Size.square(10), ), + icon: const Icon(SpotubeIcons.queueAdd), + onPressed: isLoading ? null : onAddToQueuePressed, ), - ), - if (description != null) - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: AutoSizeText( - description!, - maxLines: 2, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(.5), - ), - overflow: TextOverflow.ellipsis, - ), - ), + const SizedBox(height: 5), + IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.primary, + minimumSize: const Size.square(10), ), - const SizedBox(height: 10), + icon: isLoading + ? SizedBox.fromSize( + size: const Size.square(15), + child: const CircularProgressIndicator(strokeWidth: 2), + ) + : isPlaying + ? const Icon(SpotubeIcons.pause) + : const Icon(SpotubeIcons.play), + onPressed: isLoading ? null : onPlaybuttonPressed, + ), ], ), ), - ), + ], ); } } diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart index 1eee7d4bd..077e24e3c 100644 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ b/lib/components/shared/shimmers/shimmer_artist_profile.dart @@ -21,12 +21,13 @@ class ShimmerArtistProfile extends HookWidget { shimmerTheme.shimmerBackgroundColor ?? Colors.grey; final avatarWidth = useBreakpointValue( - sm: MediaQuery.of(context).size.width * 0.80, - md: MediaQuery.of(context).size.width * 0.50, - lg: MediaQuery.of(context).size.width * 0.30, - xl: MediaQuery.of(context).size.width * 0.30, - xxl: MediaQuery.of(context).size.width * 0.30, - ); + sm: MediaQuery.of(context).size.width * 0.80, + md: MediaQuery.of(context).size.width * 0.50, + lg: MediaQuery.of(context).size.width * 0.30, + xl: MediaQuery.of(context).size.width * 0.30, + xxl: MediaQuery.of(context).size.width * 0.30, + ) ?? + 0; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index b3717b535..a5c5eccfd 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -51,7 +51,7 @@ class SortTracksDropdown extends StatelessWidget { }, onSelected: onChanged, tooltip: "Sort tracks", - child: const Icon(SpotubeIcons.sort), + icon: const Icon(SpotubeIcons.sort), ); } } diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index ab2e4977f..73fe5b7e8 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -100,12 +100,13 @@ class TrackCollectionView extends HookConsumerWidget { ), ), // play playlist - IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.primary, + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: theme.colorScheme.inversePrimary, ), onPressed: tracksSnapshot.data != null ? onPlay : null, - icon: Icon(isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), + child: Icon(isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), ), const SizedBox(width: 10), ]; diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index fd0b249dc..1e963c479 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -179,18 +179,16 @@ class TrackTile extends HookConsumerWidget { return AnimatedContainer( duration: const Duration(milliseconds: 500), decoration: BoxDecoration( - color: isBlackListed - ? Colors.red[100] - : isActive - ? theme.popupMenuTheme.color - : Colors.transparent, - borderRadius: BorderRadius.circular(isActive ? 10 : 0), + color: isActive + ? theme.colorScheme.surfaceVariant.withOpacity(0.5) + : Colors.transparent, + borderRadius: BorderRadius.circular(10), ), child: Material( type: MaterialType.transparency, child: Row( children: [ - if (showCheck) + if (showCheck && !isBlackListed) Checkbox( value: isChecked, onChanged: (s) => onCheckChange?.call(s), @@ -222,22 +220,21 @@ class TrackTile extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.all(8.0), - child: IconButton( - icon: Icon( - playlist?.activeTrack.id == track.value.id - ? SpotubeIcons.pause - : SpotubeIcons.play, - color: Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - hoverColor: theme.colorScheme.primary.withOpacity(0.5), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.inversePrimary, + shape: const CircleBorder(), ), onPressed: !isBlackListed ? () => onTrackPlayButtonPressed?.call( track.value, ) : null, + child: Icon( + playlist?.activeTrack.id == track.value.id + ? SpotubeIcons.pause + : SpotubeIcons.play, + ), ), ), Expanded( diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index aa3fcf5fc..4c27b2b77 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -197,7 +197,7 @@ class TracksTableView extends HookConsumerWidget { default: } }, - child: const Icon(SpotubeIcons.moreVertical), + icon: const Icon(SpotubeIcons.moreVertical), ), const SizedBox(width: 10), ], @@ -205,57 +205,81 @@ class TracksTableView extends HookConsumerWidget { ...sortedTracks.asMap().entries.map((track) { String duration = "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return InkWell( - onLongPress: () { - showCheck.value = true; - selected.value = [...selected.value, track.value.id!]; - }, - onTap: () { - if (showCheck.value) { - final alreadyChecked = - selected.value.contains(track.value.id); - if (alreadyChecked) { - selected.value = selected.value - .where((id) => id != track.value.id) - .toList(); - } else { - selected.value = [...selected.value, track.value.id!]; - } - } else { - final isBlackListed = ref.read( - BlackListNotifier.provider.select( - (blacklist) => blacklist.contains( - BlacklistedElement.track( - track.value.id!, track.value.name!), - ), - ), - ); - if (!isBlackListed) { - onTrackPlayButtonPressed?.call(track.value); - } - } - }, - child: TrackTile( - playlist, - playlistId: playlistId, - track: track, - duration: duration, - userPlaylist: userPlaylist, - isActive: playlist?.activeTrack.id == track.value.id, - onTrackPlayButtonPressed: onTrackPlayButtonPressed, - isChecked: selected.value.contains(track.value.id), - showCheck: showCheck.value, - onCheckChange: (checked) { - if (checked == true) { - selected.value = [...selected.value, track.value.id!]; - } else { - selected.value = selected.value - .where((id) => id != track.value.id) - .toList(); - } - }, - ), - ); + return Consumer(builder: (context, ref, _) { + final isBlackListed = ref.watch( + BlackListNotifier.provider.select( + (blacklist) => blacklist.contains( + BlacklistedElement.track( + track.value.id!, track.value.name!), + ), + ), + ); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onLongPress: isBlackListed + ? null + : () { + showCheck.value = true; + selected.value = [ + ...selected.value, + track.value.id! + ]; + }, + onTap: isBlackListed + ? null + : () { + if (showCheck.value) { + final alreadyChecked = + selected.value.contains(track.value.id); + if (alreadyChecked) { + selected.value = selected.value + .where((id) => id != track.value.id) + .toList(); + } else { + selected.value = [ + ...selected.value, + track.value.id! + ]; + } + } else { + final isBlackListed = ref.read( + BlackListNotifier.provider.select( + (blacklist) => blacklist.contains( + BlacklistedElement.track( + track.value.id!, track.value.name!), + ), + ), + ); + if (!isBlackListed) { + onTrackPlayButtonPressed?.call(track.value); + } + } + }, + child: TrackTile( + playlist, + playlistId: playlistId, + track: track, + duration: duration, + userPlaylist: userPlaylist, + isActive: playlist?.activeTrack.id == track.value.id, + onTrackPlayButtonPressed: onTrackPlayButtonPressed, + isChecked: selected.value.contains(track.value.id), + showCheck: showCheck.value, + onCheckChange: (checked) { + if (checked == true) { + selected.value = [...selected.value, track.value.id!]; + } else { + selected.value = selected.value + .where((id) => id != track.value.id) + .toList(); + } + }, + ), + ), + ); + }); }).toList(), ]; diff --git a/lib/hooks/use_breakpoint_value.dart b/lib/hooks/use_breakpoint_value.dart index d50482ac6..e7abf08b6 100644 --- a/lib/hooks/use_breakpoint_value.dart +++ b/lib/hooks/use_breakpoint_value.dart @@ -1,6 +1,6 @@ import 'package:spotube/hooks/use_breakpoints.dart'; -useBreakpointValue({ +T useBreakpointValue({ T? sm, T? md, T? lg, @@ -8,17 +8,37 @@ useBreakpointValue({ T? xxl, T? others, }) { + final isSomeNull = + sm == null || md == null || lg == null || xl == null || xxl == null; + assert( + (isSomeNull && others != null) || (!isSomeNull && others == null), + 'You must provide a value for all breakpoints or a default value for others', + ); final breakpoint = useBreakpoints(); - if (breakpoint.isSm) { - return sm ?? others; - } else if (breakpoint.isMd) { - return md ?? others; - } else if (breakpoint.isXl) { - return xl ?? others; - } else if (breakpoint.isXxl) { - return xxl ?? others; + if (isSomeNull) { + if (breakpoint.isSm) { + return sm ?? others!; + } else if (breakpoint.isMd) { + return md ?? others!; + } else if (breakpoint.isXl) { + return xl ?? others!; + } else if (breakpoint.isXxl) { + return xxl ?? others!; + } else { + return lg ?? others!; + } } else { - return lg ?? others; + if (breakpoint.isSm) { + return sm; + } else if (breakpoint.isMd) { + return md; + } else if (breakpoint.isXl) { + return xl; + } else if (breakpoint.isXxl) { + return xxl; + } else { + return lg; + } } }