fixes to shuffle and queue
This commit is contained in:
parent
351d47b3ff
commit
5bf85a5f44
3 changed files with 312 additions and 55 deletions
|
|
@ -18,7 +18,6 @@ class HomeScreen extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final client = ref.watch(subsonicClientProvider);
|
||||
final recentAlbums = ref.watch(recentAlbumsProvider);
|
||||
final rediscover = ref.watch(rediscoverProvider);
|
||||
final newest = ref.watch(newestAlbumsProvider);
|
||||
final random = ref.watch(randomAlbumsProvider);
|
||||
|
||||
|
|
@ -62,31 +61,9 @@ class HomeScreen extends ConsumerWidget {
|
|||
artFor: artFor,
|
||||
),
|
||||
|
||||
// Made For You — rediscover artists you're neglecting.
|
||||
if (rediscover.isNotEmpty)
|
||||
_Shelf(
|
||||
title: 'Made For You',
|
||||
onShuffle: () => ref.read(rediscoverSeedProvider.notifier).state++,
|
||||
cards: [
|
||||
for (final a in rediscover)
|
||||
_ArtCard(
|
||||
artUri: null,
|
||||
title: a.name,
|
||||
subtitle: 'Artist',
|
||||
fallbackIcon: Icons.person_outline,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ArtistScreen(id: a.artistId),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Random — server discovery shelf, shares the shuffle affordance.
|
||||
_AlbumShelf(
|
||||
title: 'Random',
|
||||
albums: random,
|
||||
// Random — a single spotlighted album, re-rolled via the shuffle action.
|
||||
_RandomAlbum(
|
||||
album: random,
|
||||
artFor: artFor,
|
||||
onShuffle: () => ref.read(rediscoverSeedProvider.notifier).state++,
|
||||
),
|
||||
|
|
@ -262,14 +239,12 @@ class _HeroShell extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// A horizontal shelf: a header (with an optional shuffle action) over a
|
||||
/// scrolling row of art cards.
|
||||
/// A horizontal shelf: a header over a scrolling row of art cards.
|
||||
class _Shelf extends StatelessWidget {
|
||||
const _Shelf({required this.title, required this.cards, this.onShuffle});
|
||||
const _Shelf({required this.title, required this.cards});
|
||||
|
||||
final String title;
|
||||
final List<Widget> cards;
|
||||
final VoidCallback? onShuffle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -279,7 +254,7 @@ class _Shelf extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ShelfHeader(title: title, onShuffle: onShuffle),
|
||||
_ShelfHeader(title: title),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
SizedBox(
|
||||
height: 182,
|
||||
|
|
@ -335,26 +310,22 @@ class _AlbumShelf extends StatelessWidget {
|
|||
required this.title,
|
||||
required this.albums,
|
||||
required this.artFor,
|
||||
this.onShuffle,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final AsyncValue<List<Album>> albums;
|
||||
final String? Function(String? coverArt, {int size}) artFor;
|
||||
final VoidCallback? onShuffle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return albums.when(
|
||||
loading: () => _Shelf(
|
||||
title: title,
|
||||
onShuffle: onShuffle,
|
||||
cards: const [_ArtCardSkeleton(), _ArtCardSkeleton(), _ArtCardSkeleton()],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data: (list) => _Shelf(
|
||||
title: title,
|
||||
onShuffle: onShuffle,
|
||||
cards: [
|
||||
for (final a in list)
|
||||
_ArtCard(
|
||||
|
|
@ -371,6 +342,134 @@ class _AlbumShelf extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Random spotlight — a single album shown as cover-left / details-right.
|
||||
/// Details are the album title, artist, release year and genre. Re-rolls via
|
||||
/// the header's shuffle affordance. Hidden while loading errors or is empty.
|
||||
class _RandomAlbum extends StatelessWidget {
|
||||
const _RandomAlbum({
|
||||
required this.album,
|
||||
required this.artFor,
|
||||
this.onShuffle,
|
||||
});
|
||||
|
||||
final AsyncValue<List<Album>> album;
|
||||
final String? Function(String? coverArt, {int size}) artFor;
|
||||
final VoidCallback? onShuffle;
|
||||
|
||||
static const double _size = 132;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Album? a = album.maybeWhen(
|
||||
data: (list) => list.isEmpty ? null : list.first,
|
||||
orElse: () => null,
|
||||
);
|
||||
final bool loading = album.isLoading;
|
||||
if (a == null && !loading) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: TimbreSpacing.xl),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ShelfHeader(title: 'Random', onShuffle: onShuffle),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
if (a == null)
|
||||
const _RandomSkeleton()
|
||||
else
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => AlbumScreen(id: a.id)),
|
||||
),
|
||||
child: _RandomBody(album: a, artUri: artFor(a.coverArt)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RandomBody extends StatelessWidget {
|
||||
const _RandomBody({required this.album, required this.artUri});
|
||||
|
||||
final Album album;
|
||||
final String? artUri;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SizedBox(
|
||||
width: _RandomAlbum._size,
|
||||
height: _RandomAlbum._size,
|
||||
child: ColoredBox(
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(artUri!,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => const Icon(
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: const Icon(Icons.album_outlined, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(album.name ?? 'Unknown album',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
if (album.artist != null)
|
||||
Text(album.artist!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
if (album.year != null)
|
||||
Text('${album.year}',
|
||||
style: const TextStyle(
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
if (album.genre != null)
|
||||
Text(album.genre!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RandomSkeleton extends StatelessWidget {
|
||||
const _RandomSkeleton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: const SizedBox(
|
||||
width: _RandomAlbum._size,
|
||||
height: _RandomAlbum._size,
|
||||
child: ColoredBox(color: TimbreColors.surface),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A fixed-width art tile with a caption — the shelf's building block.
|
||||
class _ArtCard extends StatelessWidget {
|
||||
const _ArtCard({
|
||||
|
|
@ -378,14 +477,12 @@ class _ArtCard extends StatelessWidget {
|
|||
required this.artUri,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
this.fallbackIcon = Icons.album_outlined,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? artUri;
|
||||
final String? subtitle;
|
||||
final VoidCallback? onTap;
|
||||
final IconData fallbackIcon;
|
||||
|
||||
static const double _size = 132;
|
||||
|
||||
|
|
@ -410,9 +507,10 @@ class _ArtCard extends StatelessWidget {
|
|||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) =>
|
||||
Icon(fallbackIcon, color: TimbreColors.dimmed))
|
||||
: Icon(fallbackIcon, color: TimbreColors.dimmed),
|
||||
errorBuilder: (_, _, _) => const Icon(
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: const Icon(Icons.album_outlined,
|
||||
color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue