feat(flutter): extend metadata prefetch to library tabs + artist detail
MetadataPrefetcher gains warmAlbums(ids) / warmArtists(ids) public methods so callers can fan out drift-cache warm-ups for whatever collection just landed. Wired into: - Library Artists tab — warms first 8 artists from the page on every data emit (initial + paginate + refresh). - Library Albums tab — same for first 8 albums. - Artist detail album grid — warms first 8 albums from the artist's album list as soon as it loads, so tapping into any of them is a drift hit. Hard-cap of 8 per call (same as the home prefetch). Set spans across calls aren't deduped at this layer because providers themselves short-circuit on cached values. Also instrument the artist-detail play button: try/catch around the artistTracksProvider read + playTracks call, snackbar on empty-tracks or thrown-error so silent failures stop being silent. The current behavior was an early return on tracks.isEmpty with no visible feedback.
This commit is contained in:
+26
-5
@@ -5,11 +5,9 @@ import '../library/library_providers.dart';
|
|||||||
import '../models/home_data.dart';
|
import '../models/home_data.dart';
|
||||||
|
|
||||||
/// Pre-warms the drift cache for entities the user is most likely to
|
/// Pre-warms the drift cache for entities the user is most likely to
|
||||||
/// tap. When /api/home returns its sections, fire one albumProvider /
|
/// tap. The home prefetch fires when /api/home returns. Library
|
||||||
/// artistProvider read per top-of-section item in the background. The
|
/// tabs and artist detail expose `warmAlbums` / `warmArtists` so they
|
||||||
/// reads trigger the existing cold-cache path, which writes drift.
|
/// can fan out reads from their own AsyncNotifier callbacks.
|
||||||
/// By the time the user actually taps a tile, it's a drift hit and
|
|
||||||
/// the detail screen renders instantly.
|
|
||||||
///
|
///
|
||||||
/// All reads are unawaited and idempotent — providers that already
|
/// All reads are unawaited and idempotent — providers that already
|
||||||
/// have a cached value short-circuit. Readers/writers don't conflict
|
/// have a cached value short-circuit. Readers/writers don't conflict
|
||||||
@@ -28,6 +26,29 @@ class MetadataPrefetcher {
|
|||||||
/// the user can see without scrolling on a typical phone.
|
/// the user can see without scrolling on a typical phone.
|
||||||
static const _topN = 8;
|
static const _topN = 8;
|
||||||
|
|
||||||
|
/// Pre-warm drift for each album id (no-op past the first dedup).
|
||||||
|
/// Called from the library Albums tab + artist detail album grid.
|
||||||
|
void warmAlbums(Iterable<String> ids) {
|
||||||
|
var n = 0;
|
||||||
|
for (final id in ids) {
|
||||||
|
if (id.isEmpty) continue;
|
||||||
|
if (n++ >= _topN) break;
|
||||||
|
_swallow(_ref.read(albumProvider(id).future));
|
||||||
|
}
|
||||||
|
if (n > 0) debugPrint('metadataPrefetcher: warming $n albums');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-warm drift for each artist id.
|
||||||
|
void warmArtists(Iterable<String> ids) {
|
||||||
|
var n = 0;
|
||||||
|
for (final id in ids) {
|
||||||
|
if (id.isEmpty) continue;
|
||||||
|
if (n++ >= _topN) break;
|
||||||
|
_swallow(_ref.read(artistProvider(id).future));
|
||||||
|
}
|
||||||
|
if (n > 0) debugPrint('metadataPrefetcher: warming $n artists');
|
||||||
|
}
|
||||||
|
|
||||||
void _warmHome(HomeData h) {
|
void _warmHome(HomeData h) {
|
||||||
final albumIds = <String>{};
|
final albumIds = <String>{};
|
||||||
final artistIds = <String>{};
|
final artistIds = <String>{};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show debugPrint;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../api/endpoints/likes.dart';
|
import '../api/endpoints/likes.dart';
|
||||||
|
import '../cache/metadata_prefetcher.dart';
|
||||||
import '../likes/like_button.dart';
|
import '../likes/like_button.dart';
|
||||||
import '../models/album.dart';
|
import '../models/album.dart';
|
||||||
import '../models/artist.dart';
|
import '../models/artist.dart';
|
||||||
@@ -92,10 +94,33 @@ class ArtistDetailScreen extends ConsumerWidget {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(Icons.play_arrow, color: fs.parchment),
|
icon: Icon(Icons.play_arrow, color: fs.parchment),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final tracks = await ref.read(artistTracksProvider(id).future);
|
try {
|
||||||
if (tracks.isEmpty) return;
|
final tracks =
|
||||||
final shuffled = [...tracks]..shuffle();
|
await ref.read(artistTracksProvider(id).future);
|
||||||
ref.read(playerActionsProvider).playTracks(shuffled);
|
debugPrint(
|
||||||
|
'artist_detail: play tapped — ${tracks.length} tracks for $id');
|
||||||
|
if (tracks.isEmpty) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'No tracks found for this artist yet.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final shuffled = [...tracks]..shuffle();
|
||||||
|
await ref
|
||||||
|
.read(playerActionsProvider)
|
||||||
|
.playTracks(shuffled);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('artist_detail: play failed: $e');
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Couldn't start playback: $e")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -109,7 +134,11 @@ class ArtistDetailScreen extends ConsumerWidget {
|
|||||||
albums.when(
|
albums.when(
|
||||||
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
||||||
loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator())),
|
loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator())),
|
||||||
data: (list) => LayoutBuilder(builder: (ctx, constraints) {
|
data: (list) {
|
||||||
|
ref
|
||||||
|
.read(metadataPrefetcherProvider)
|
||||||
|
.warmAlbums(list.map((a) => a.id));
|
||||||
|
return LayoutBuilder(builder: (ctx, constraints) {
|
||||||
const cols = 3;
|
const cols = 3;
|
||||||
const sidePad = 8.0;
|
const sidePad = 8.0;
|
||||||
const gap = 8.0;
|
const gap = 8.0;
|
||||||
@@ -144,7 +173,8 @@ class ArtistDetailScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../api/endpoints/library_lists.dart';
|
import '../api/endpoints/library_lists.dart';
|
||||||
import '../api/endpoints/likes.dart';
|
import '../api/endpoints/likes.dart';
|
||||||
import '../api/endpoints/me.dart';
|
import '../api/endpoints/me.dart';
|
||||||
|
import '../cache/metadata_prefetcher.dart';
|
||||||
import '../library/library_providers.dart' show dioProvider;
|
import '../library/library_providers.dart' show dioProvider;
|
||||||
import '../models/album.dart';
|
import '../models/album.dart';
|
||||||
import '../models/artist.dart';
|
import '../models/artist.dart';
|
||||||
@@ -207,7 +208,12 @@ class _ArtistsTab extends ConsumerWidget {
|
|||||||
return ref.watch(_libraryArtistsProvider).when(
|
return ref.watch(_libraryArtistsProvider).when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
||||||
data: (page) => page.items.isEmpty
|
data: (page) {
|
||||||
|
// Warm details for the first screenful so taps are instant.
|
||||||
|
ref
|
||||||
|
.read(metadataPrefetcherProvider)
|
||||||
|
.warmArtists(page.items.map((a) => a.id));
|
||||||
|
return page.items.isEmpty
|
||||||
? Center(child: Text("No artists yet — scan a library folder via the server's config.", style: TextStyle(color: fs.ash), textAlign: TextAlign.center))
|
? Center(child: Text("No artists yet — scan a library folder via the server's config.", style: TextStyle(color: fs.ash), textAlign: TextAlign.center))
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@@ -247,7 +253,8 @@ class _ArtistsTab extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +268,11 @@ class _AlbumsTab extends ConsumerWidget {
|
|||||||
return ref.watch(_libraryAlbumsProvider).when(
|
return ref.watch(_libraryAlbumsProvider).when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))),
|
||||||
data: (page) => page.items.isEmpty
|
data: (page) {
|
||||||
|
ref
|
||||||
|
.read(metadataPrefetcherProvider)
|
||||||
|
.warmAlbums(page.items.map((a) => a.id));
|
||||||
|
return page.items.isEmpty
|
||||||
? Center(child: Text("No albums yet — scan a library folder via the server's config.", style: TextStyle(color: fs.ash), textAlign: TextAlign.center))
|
? Center(child: Text("No albums yet — scan a library folder via the server's config.", style: TextStyle(color: fs.ash), textAlign: TextAlign.center))
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
@@ -315,7 +326,8 @@ class _AlbumsTab extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user