offline updates and playhead fix
This commit is contained in:
parent
7a199fe4df
commit
6663330260
14 changed files with 1673 additions and 545 deletions
|
|
@ -5,6 +5,7 @@ import '../history/play_history.dart';
|
|||
import '../subsonic/models.dart';
|
||||
import '../state/providers.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import '../widgets/art_image.dart';
|
||||
import '../widgets/block_progress_bar.dart';
|
||||
import 'browser_screen.dart';
|
||||
|
||||
|
|
@ -22,9 +23,7 @@ class HomeScreen extends ConsumerWidget {
|
|||
final random = ref.watch(randomAlbumsProvider);
|
||||
|
||||
String? artFor(String? coverArt, {int size = 300}) =>
|
||||
(client != null && coverArt != null)
|
||||
? client.coverArtUri(coverArt, size: size).toString()
|
||||
: null;
|
||||
resolveArtUriW(ref, coverArt: coverArt, size: size)?.toString();
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
|
|
@ -55,11 +54,7 @@ class HomeScreen extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Recently Added — server discovery shelf.
|
||||
_AlbumShelf(
|
||||
title: 'Recently Added',
|
||||
albums: newest,
|
||||
artFor: artFor,
|
||||
),
|
||||
_AlbumShelf(title: 'Recently Added', albums: newest, artFor: artFor),
|
||||
|
||||
// Random — a single spotlighted album, re-rolled via the shuffle action.
|
||||
_RandomAlbum(
|
||||
|
|
@ -81,9 +76,9 @@ class HomeScreen extends ConsumerWidget {
|
|||
}
|
||||
|
||||
static void _pushAlbum(BuildContext context, String albumId) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => AlbumScreen(id: albumId)),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => AlbumScreen(id: albumId)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +91,6 @@ class _HeroCard extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
final client = ref.watch(subsonicClientProvider);
|
||||
final current = ref.watch(activePlaybackProvider.select((s) => s.current));
|
||||
|
||||
// Fall back to the most recent track so the hero is useful before playback.
|
||||
|
|
@ -104,9 +98,11 @@ class _HeroCard extends ConsumerWidget {
|
|||
final PlayRecord? fallback = recent.isEmpty ? null : recent.first;
|
||||
|
||||
final String? coverArt = current?.coverArt ?? fallback?.coverArt;
|
||||
final artUri = (client != null && coverArt != null)
|
||||
? client.coverArtUri(coverArt, size: 240).toString()
|
||||
: null;
|
||||
final artUri = resolveArtUriW(
|
||||
ref,
|
||||
coverArt: coverArt,
|
||||
size: 240,
|
||||
)?.toString();
|
||||
|
||||
final title = current?.title ?? fallback?.title;
|
||||
final subtitle = current?.artist ?? fallback?.artist;
|
||||
|
|
@ -118,12 +114,17 @@ class _HeroCard extends ConsumerWidget {
|
|||
onTap: () => ref.read(selectedTabProvider.notifier).state = 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.library_music_outlined,
|
||||
color: TimbreColors.dimmed, size: 40),
|
||||
Icon(
|
||||
Icons.library_music_outlined,
|
||||
color: TimbreColors.dimmed,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
child: Text('Browse your library to start listening',
|
||||
style: TextStyle(color: TimbreColors.foreground)),
|
||||
child: Text(
|
||||
'Browse your library to start listening',
|
||||
style: TextStyle(color: TimbreColors.foreground),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -145,18 +146,7 @@ class _HeroCard extends ConsumerWidget {
|
|||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: ColoredBox(
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(artUri,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => Icon(
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: Icon(Icons.album_outlined,
|
||||
color: TimbreColors.dimmed),
|
||||
),
|
||||
child: ArtImage(artUri, fit: BoxFit.cover),
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
Expanded(
|
||||
|
|
@ -166,29 +156,40 @@ class _HeroCard extends ConsumerWidget {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(hasCurrent ? Icons.play_arrow : Icons.history,
|
||||
size: 14, color: accent),
|
||||
Icon(
|
||||
hasCurrent ? Icons.play_arrow : Icons.history,
|
||||
size: 14,
|
||||
color: accent,
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.xs),
|
||||
Text(hasCurrent ? 'NOW PLAYING' : 'RESUME',
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontSize: 11,
|
||||
letterSpacing: 1,
|
||||
fontWeight: FontWeight.w700)),
|
||||
Text(
|
||||
hasCurrent ? 'NOW PLAYING' : 'RESUME',
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontSize: 11,
|
||||
letterSpacing: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: TimbreSpacing.xs),
|
||||
Text(title,
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: TimbreColors.dimmed)),
|
||||
style: TextStyle(color: TimbreColors.dimmed),
|
||||
),
|
||||
if (hasCurrent) ...[
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
const _HeroProgress(),
|
||||
|
|
@ -210,14 +211,19 @@ class _HeroProgress extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final progress = ref.watch(activePlaybackProvider.select((s) => s.progress));
|
||||
final progress = ref.watch(
|
||||
activePlaybackProvider.select((s) => s.progress),
|
||||
);
|
||||
return BlockProgressBar(progress: progress, cells: 32, height: 6);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeroShell extends StatelessWidget {
|
||||
const _HeroShell(
|
||||
{required this.child, required this.accent, required this.onTap});
|
||||
const _HeroShell({
|
||||
required this.child,
|
||||
required this.accent,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Color accent;
|
||||
|
|
@ -282,11 +288,14 @@ class _ShelfHeader extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (onShuffle != null)
|
||||
InkWell(
|
||||
|
|
@ -321,7 +330,11 @@ class _AlbumShelf extends StatelessWidget {
|
|||
return albums.when(
|
||||
loading: () => _Shelf(
|
||||
title: title,
|
||||
cards: const [_ArtCardSkeleton(), _ArtCardSkeleton(), _ArtCardSkeleton()],
|
||||
cards: const [
|
||||
_ArtCardSkeleton(),
|
||||
_ArtCardSkeleton(),
|
||||
_ArtCardSkeleton(),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data: (list) => _Shelf(
|
||||
|
|
@ -332,9 +345,9 @@ class _AlbumShelf extends StatelessWidget {
|
|||
artUri: artFor(a.coverArt),
|
||||
title: a.name ?? 'Unknown album',
|
||||
subtitle: a.artist,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => AlbumScreen(id: a.id)),
|
||||
),
|
||||
onTap: () => Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => AlbumScreen(id: a.id))),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -378,9 +391,9 @@ class _RandomAlbum extends StatelessWidget {
|
|||
const _RandomSkeleton()
|
||||
else
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => AlbumScreen(id: a.id)),
|
||||
),
|
||||
onTap: () => Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => AlbumScreen(id: a.id))),
|
||||
child: _RandomBody(album: a, artUri: artFor(a.coverArt)),
|
||||
),
|
||||
],
|
||||
|
|
@ -402,20 +415,11 @@ class _RandomBody extends StatelessWidget {
|
|||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SizedBox(
|
||||
child: ArtImage(
|
||||
artUri,
|
||||
fit: BoxFit.cover,
|
||||
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: (_, _, _) => Icon(
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: Icon(Icons.album_outlined, color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.lg),
|
||||
|
|
@ -424,28 +428,35 @@ class _RandomBody extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(album.name ?? 'Unknown album',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: FontWeight.w700)),
|
||||
Text(
|
||||
album.name ?? 'Unknown album',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: 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: TextStyle(color: TimbreColors.dimmed)),
|
||||
Text(
|
||||
album.artist!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: TimbreColors.dimmed),
|
||||
),
|
||||
if (album.year != null)
|
||||
Text('${album.year}',
|
||||
style: TextStyle(
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
Text(
|
||||
'${album.year}',
|
||||
style: TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
if (album.genre != null)
|
||||
Text(album.genre!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.dimmed, fontSize: 12)),
|
||||
Text(
|
||||
album.genre!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -497,34 +508,27 @@ class _ArtCard extends StatelessWidget {
|
|||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: SizedBox(
|
||||
child: ArtImage(
|
||||
artUri,
|
||||
fit: BoxFit.cover,
|
||||
width: _size,
|
||||
height: _size,
|
||||
child: ColoredBox(
|
||||
color: TimbreColors.surface,
|
||||
child: artUri != null
|
||||
? Image.network(artUri!,
|
||||
key: ValueKey(artUri),
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => Icon(
|
||||
Icons.album_outlined, color: TimbreColors.dimmed))
|
||||
: Icon(Icons.album_outlined,
|
||||
color: TimbreColors.dimmed),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
Text(title,
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: TimbreColors.foreground),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: TimbreColors.foreground)),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
TextStyle(color: TimbreColors.dimmed, fontSize: 12)),
|
||||
style: TextStyle(color: TimbreColors.dimmed, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue