feat(playlist,album page): play and shuffle take full width on smaller screens, add new xs breakpoint

This commit is contained in:
Kingkor Roy Tirtho 2023-06-18 12:07:26 +06:00
parent 7a8bd92104
commit dce1b88694
25 changed files with 276 additions and 177 deletions

View File

@ -53,7 +53,7 @@ class AlbumCard extends HookConsumerWidget {
[playlistNotifier, query?.data, album.tracks],
);
final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);

View File

@ -35,6 +35,7 @@ class ArtistCard extends HookConsumerWidget {
final radius = BorderRadius.circular(15);
final double size = useBreakpointValue<double>(
xs: 130,
sm: 130,
md: 150,
others: 170,

View File

@ -188,7 +188,7 @@ class _MultiSelectDialog<T> extends HookWidget {
return AlertDialog(
scrollable: true,
title: dialogTitle ?? const Text('Select'),
contentPadding: mediaQuery.isSm ? const EdgeInsets.all(16) : null,
contentPadding: mediaQuery.mdAndUp ? null : const EdgeInsets.all(16),
insetPadding: const EdgeInsets.all(16),
actions: [
OutlinedButton(

View File

@ -24,6 +24,7 @@ class UserAlbums extends HookConsumerWidget {
final albumsQuery = useQueries.album.ofMine(ref);
final spacing = useBreakpointValue<double>(
xs: 0,
sm: 0,
others: 20,
);

View File

@ -39,7 +39,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
),
),
),
if (mediaQuery.isSm || mediaQuery.isMd)
if (mediaQuery.mdAndDown)
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -121,7 +121,7 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context);
final spotify = ref.watch(spotifyProvider);
if (mediaQuery.isSm) {
if (mediaQuery.smAndDown) {
return ElevatedButton(
style: FilledButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.primary,

View File

@ -58,8 +58,7 @@ class BottomPlayer extends HookConsumerWidget {
// returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries]
if (layoutMode == LayoutMode.compact ||
((mediaQuery.isSm || mediaQuery.isMd) &&
layoutMode == LayoutMode.adaptive)) {
((mediaQuery.mdAndDown) && layoutMode == LayoutMode.adaptive)) {
return PlayerOverlay(albumArt: albumArt);
}

View File

@ -82,16 +82,17 @@ class Sidebar extends HookConsumerWidget {
}, [controller]);
useEffect(() {
if (!context.mounted) return;
if (mediaQuery.lgAndUp && !controller.extended) {
controller.setExtended(true);
} else if ((mediaQuery.isSm || mediaQuery.isMd) && controller.extended) {
} else if (mediaQuery.mdAndDown && controller.extended) {
controller.setExtended(false);
}
return null;
}, [mediaQuery, controller]);
if (layoutMode == LayoutMode.compact ||
(mediaQuery.isSm && layoutMode == LayoutMode.adaptive)) {
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
return Scaffold(body: child);
}
@ -186,7 +187,7 @@ class SidebarHeader extends HookWidget {
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
if (mediaQuery.isSm || mediaQuery.isMd) {
if (mediaQuery.mdAndDown) {
return Container(
height: 40,
width: 40,
@ -236,7 +237,7 @@ class SidebarFooter extends HookConsumerWidget {
final auth = ref.watch(AuthenticationNotifier.provider);
if (mediaQuery.isSm || mediaQuery.isMd) {
if (mediaQuery.mdAndDown) {
return IconButton(
icon: const Icon(SpotubeIcons.settings),
onPressed: () => Sidebar.goToSettings(context),

View File

@ -27,10 +27,11 @@ class AdaptiveListTile extends HookWidget {
return ListTile(
title: title,
subtitle: subtitle,
trailing:
breakOn ?? mediaQuery.isSm ? null : trailing?.call(context, null),
trailing: breakOn ?? mediaQuery.smAndDown
? null
: trailing?.call(context, null),
leading: leading,
onTap: breakOn ?? mediaQuery.isSm
onTap: breakOn ?? mediaQuery.smAndDown
? () {
onTap?.call();
showDialog(

View File

@ -40,6 +40,7 @@ class PlaybuttonCard extends HookWidget {
final radius = BorderRadius.circular(15);
final double size = useBreakpointValue<double>(
xs: 130,
sm: 130,
md: 150,
others: 170,
@ -47,6 +48,7 @@ class PlaybuttonCard extends HookWidget {
170;
final end = useBreakpointValue<double>(
xs: 15,
sm: 15,
others: 20,
) ??

View File

@ -21,6 +21,7 @@ class ShimmerArtistProfile extends HookWidget {
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
final avatarWidth = useBreakpointValue(
xs: MediaQuery.of(context).size.width * 0.80,
sm: MediaQuery.of(context).size.width * 0.80,
md: MediaQuery.of(context).size.width * 0.50,
lg: MediaQuery.of(context).size.width * 0.30,

View File

@ -18,6 +18,7 @@ class ShimmerCategories extends HookWidget {
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
final shimmerCount = useBreakpointValue(
xs: 2,
sm: 2,
md: 3,
lg: 3,

View File

@ -32,7 +32,7 @@ class ShimmerLyrics extends HookWidget {
if (mediaQuery.isMd) {
widthsCp.removeLast();
}
if (mediaQuery.isSm) {
if (mediaQuery.smAndDown) {
widthsCp.removeLast();
widthsCp.removeLast();
}

View File

@ -86,6 +86,7 @@ class ShimmerPlaybuttonCard extends HookWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Size size = useBreakpointValue<Size>(
xs: const Size(130, 200),
sm: const Size(130, 200),
md: const Size(150, 220),
others: const Size(170, 240),

View File

@ -18,6 +18,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
);
final breakpoint = useBreakpointValue(
xs: 85.0,
sm: 85.0,
md: 35.0,
others: 0.0,

View File

@ -0,0 +1,195 @@
import 'dart:ui';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
class TrackCollectionHeading<T> extends HookConsumerWidget {
final String title;
final String? description;
final String titleImage;
final List<Widget> buttons;
final AlbumSimple? album;
final Query<List<TrackSimple>, T> tracksSnapshot;
final bool isPlaying;
final void Function([Track? currentTrack]) onPlay;
final void Function([Track? currentTrack]) onShuffledPlay;
final PaletteColor? color;
const TrackCollectionHeading({
Key? key,
required this.title,
required this.titleImage,
required this.buttons,
required this.tracksSnapshot,
required this.isPlaying,
required this.onPlay,
required this.onShuffledPlay,
required this.color,
this.description,
this.album,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
return LayoutBuilder(
builder: (context, constrains) {
return DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(titleImage),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black45,
theme.colorScheme.surface,
],
begin: const FractionalOffset(0, 0),
end: const FractionalOffset(0, 1),
tileMode: TileMode.clamp,
),
),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Flex(
direction:
constrains.mdAndDown ? Axis.vertical : Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
path: titleImage,
placeholder: Assets.albumPlaceholder.path,
),
),
),
const SizedBox(width: 10, height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Text(
title,
style: theme.textTheme.titleLarge!.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
if (album != null)
Text(
"${AlbumType.from(album?.albumType).formatted}${context.l10n.released}${DateTime.tryParse(
album?.releaseDate ?? "",
)?.year}",
style: theme.textTheme.titleMedium!.copyWith(
color: Colors.white,
fontWeight: FontWeight.normal,
),
),
if (description != null)
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constrains.mdAndDown ? 400 : 300,
),
child: Text(
description!,
style: const TextStyle(color: Colors.white),
maxLines: 2,
overflow: TextOverflow.fade,
),
),
const SizedBox(height: 10),
IconTheme(
data: theme.iconTheme.copyWith(
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: buttons,
),
),
const SizedBox(height: 10),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constrains.mdAndDown ? 400 : 300,
),
child: Row(
mainAxisSize: constrains.smAndUp
? MainAxisSize.min
: MainAxisSize.min,
children: [
Expanded(
child: FilledButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: color?.color,
),
label: Text(context.l10n.shuffle),
icon: const Icon(SpotubeIcons.shuffle),
onPressed:
tracksSnapshot.data == null || isPlaying
? null
: onShuffledPlay,
),
),
const SizedBox(width: 10),
Expanded(
child: FilledButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: color?.color,
foregroundColor: color?.bodyTextColor,
),
onPressed: tracksSnapshot.data != null
? onPlay
: null,
icon: Icon(
isPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
),
label: Text(
isPlaying
? context.l10n.stop
: context.l10n.play,
),
),
),
],
),
),
],
)
],
),
),
),
),
),
);
},
);
}
}

View File

@ -1,16 +1,12 @@
import 'dart:ui';
import 'package:fl_query/fl_query.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart';
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_custom_status_bar_color.dart';
@ -31,8 +27,8 @@ class TrackCollectionView<T> extends HookConsumerWidget {
final String titleImage;
final bool isPlaying;
final void Function([Track? currentTrack]) onPlay;
final void Function() onAddToQueue;
final void Function([Track? currentTrack]) onShuffledPlay;
final void Function() onAddToQueue;
final void Function() onShare;
final Widget? heartBtn;
final AlbumSimple? album;
@ -187,145 +183,17 @@ class TrackCollectionView<T> extends HookConsumerWidget {
: null,
centerTitle: true,
flexibleSpace: FlexibleSpaceBar(
background: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(titleImage),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black45,
theme.colorScheme.surface,
],
begin: const FractionalOffset(0, 0),
end: const FractionalOffset(0, 1),
tileMode: TileMode.clamp,
),
),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Wrap(
spacing: 20,
runSpacing: 20,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
children: [
Container(
constraints:
const BoxConstraints(maxHeight: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
path: titleImage,
placeholder:
Assets.albumPlaceholder.path,
),
),
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: theme.textTheme.titleLarge!
.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
if (album != null)
Text(
"${AlbumType.from(album?.albumType).formatted}${context.l10n.released}${DateTime.tryParse(
album?.releaseDate ?? "",
)?.year}",
style: theme.textTheme.titleMedium!
.copyWith(
color: Colors.white,
fontWeight: FontWeight.normal,
),
),
if (description != null)
Text(
description!,
style: const TextStyle(
color: Colors.white),
maxLines: 2,
overflow: TextOverflow.fade,
),
const SizedBox(height: 10),
IconTheme(
data: theme.iconTheme.copyWith(
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: buttons,
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: color?.color,
),
label: Text(context.l10n.shuffle),
icon: const Icon(
SpotubeIcons.shuffle),
onPressed:
tracksSnapshot.data == null ||
isPlaying
? null
: onShuffledPlay,
),
const SizedBox(width: 10),
FilledButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: color?.color,
foregroundColor:
color?.bodyTextColor,
),
onPressed:
tracksSnapshot.data != null
? onPlay
: null,
icon: Icon(
isPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
),
label: Text(
isPlaying
? context.l10n.stop
: context.l10n.play,
),
),
],
),
],
)
],
),
),
),
),
),
background: TrackCollectionHeading<T>(
color: color,
title: title,
description: description,
titleImage: titleImage,
isPlaying: isPlaying,
onPlay: onPlay,
onShuffledPlay: onShuffledPlay,
tracksSnapshot: tracksSnapshot,
buttons: buttons,
album: album,
),
),
),
@ -361,7 +229,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
// scroll the flexible space
// to allow more space for search results
controller.animateTo(
390,
330,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);

View File

@ -61,7 +61,7 @@ class TrackTile extends HookConsumerWidget {
return LayoutBuilder(builder: (context, constrains) {
return HoverBuilder(
permanentState: isPlaying || constrains.isSm ? true : null,
permanentState: isPlaying || constrains.smAndDown ? true : null,
builder: (context, isHovering) {
return ListTile(
selected: isPlaying,
@ -89,7 +89,7 @@ class TrackTile extends HookConsumerWidget {
),
),
)
else if (constrains.isSm)
else if (constrains.smAndDown)
const SizedBox(width: 16),
if (onChanged != null)
Checkbox.adaptive(

View File

@ -390,6 +390,7 @@ class TracksTableView extends HookConsumerWidget {
if (isSliver) {
return SliverSafeArea(
top: false,
sliver: SliverList(delegate: SliverChildListDelegate(children)),
);
}

View File

@ -1,25 +1,39 @@
import 'package:flutter/widgets.dart';
extension ContainerBreakpoints on BoxConstraints {
bool get isSm => biggest.width <= 640;
bool get isXs => biggest.width <= 480;
bool get isSm => biggest.width > 480 && biggest.width <= 640;
bool get isMd => biggest.width > 640 && biggest.width <= 768;
bool get isLg => biggest.width > 768 && biggest.width <= 1024;
bool get isXl => biggest.width > 1024 && biggest.width <= 1280;
bool get is2Xl => biggest.width > 1280;
bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl;
bool get mdAndUp => isMd || isLg || isXl || is2Xl;
bool get lgAndUp => isLg || isXl || is2Xl;
bool get xlAndUp => isXl || is2Xl;
bool get smAndDown => isXs || isSm;
bool get mdAndDown => isXs || isSm || isMd;
bool get lgAndDown => isXs || isSm || isMd || isLg;
bool get xlAndDown => isXs || isSm || isMd || isLg || isXl;
}
extension ScreenBreakpoints on MediaQueryData {
bool get isSm => size.width <= 640;
bool get isXs => size.width <= 480;
bool get isSm => size.width > 480 && size.width <= 640;
bool get isMd => size.width > 640 && size.width <= 768;
bool get isLg => size.width > 768 && size.width <= 1024;
bool get isXl => size.width > 1024 && size.width <= 1280;
bool get is2Xl => size.width > 1280;
bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl;
bool get mdAndUp => isMd || isLg || isXl || is2Xl;
bool get lgAndUp => isLg || isXl || is2Xl;
bool get xlAndUp => isXl || is2Xl;
bool get smAndDown => isXs || isSm;
bool get mdAndDown => isXs || isSm || isMd;
bool get lgAndDown => isXs || isSm || isMd || isLg;
bool get xlAndDown => isXs || isSm || isMd || isLg || isXl;
}

View File

@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
T useBreakpointValue<T>({
T? xs,
T? sm,
T? md,
T? lg,
@ -10,8 +11,12 @@ T useBreakpointValue<T>({
T? xxl,
T? others,
}) {
final isSomeNull =
sm == null || md == null || lg == null || xl == null || xxl == null;
final isSomeNull = xs == null ||
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',
@ -20,7 +25,9 @@ T useBreakpointValue<T>({
final mediaQuery = MediaQuery.of(context);
if (isSomeNull) {
if (mediaQuery.isSm) {
if (mediaQuery.isXs) {
return xs ?? others!;
} else if (mediaQuery.isSm) {
return sm ?? others!;
} else if (mediaQuery.isMd) {
return md ?? others!;
@ -32,7 +39,9 @@ T useBreakpointValue<T>({
return lg ?? others!;
}
} else {
if (mediaQuery.isSm) {
if (mediaQuery.isXs) {
return xs;
} else if (mediaQuery.isSm) {
return sm;
} else if (mediaQuery.isMd) {
return md;

View File

@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/components/shared/track_table/track_collection_view.dart';
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart';
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
@ -67,7 +67,7 @@ class AlbumPage extends HookConsumerWidget {
tracksSnapshot: tracksSnapshot,
album: album,
routePath: "/album/${album.id}",
bottomSpace: mediaQuery.isSm || mediaQuery.isMd,
bottomSpace: mediaQuery.mdAndDown,
onPlay: ([track]) {
if (tracksSnapshot.hasData) {
if (!isAlbumPlaying) {

View File

@ -39,6 +39,7 @@ class ArtistPage extends HookConsumerWidget {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final textTheme = theme.textTheme;
final chipTextVariant = useBreakpointValue(
xs: textTheme.bodySmall,
sm: textTheme.bodySmall,
md: textTheme.bodyMedium,
lg: textTheme.bodyLarge,
@ -49,6 +50,7 @@ class ArtistPage extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context);
final avatarWidth = useBreakpointValue(
xs: mediaQuery.size.width * 0.50,
sm: mediaQuery.size.width * 0.50,
md: mediaQuery.size.width * 0.40,
lg: mediaQuery.size.width * 0.18,
@ -155,7 +157,7 @@ class ArtistPage extends HookConsumerWidget {
),
Text(
data.name!,
style: mediaQuery.isSm
style: mediaQuery.smAndDown
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
@ -166,8 +168,9 @@ class ArtistPage extends HookConsumerWidget {
),
),
style: textTheme.bodyMedium?.copyWith(
fontWeight:
mediaQuery.isSm ? null : FontWeight.bold,
fontWeight: mediaQuery.mdAndUp
? FontWeight.bold
: null,
),
),
const SizedBox(height: 20),

View File

@ -35,7 +35,7 @@ class DesktopLoginPage extends HookConsumerWidget {
children: [
Assets.spotubeLogoPng.image(
width: MediaQuery.of(context).size.width *
(mediaQuery.isSm || mediaQuery.isMd ? .5 : .3),
(mediaQuery.mdAndDown ? .5 : .3),
),
Text(
context.l10n.add_spotify_credentials,

View File

@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/components/shared/track_table/track_collection_view.dart';
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart';
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/models/logger.dart';
@ -99,7 +99,7 @@ class PlaylistView extends HookConsumerWidget {
playlistNotifier.addTracks(tracksSnapshot.data!);
}
},
bottomSpace: mediaQuery.isSm || mediaQuery.isMd,
bottomSpace: mediaQuery.mdAndDown,
showShare: playlist.id != "user-liked-tracks",
routePath: "/playlist/${playlist.id}",
onShare: () {