fix(flutter): album/artist detail covers + cacheFirst tracing for hangs

Album detail screen rendered the cover slot as a bare slate Container
— no actual image. Wire ServerImage at /api/albums/<id>/cover (96dp,
rounded). Same for artist detail: ClipOval around ServerImage of the
artist's coverUrl, falling back to slate when empty.

Artist navigation hang: artistProvider goes through cacheFirst, which
had no logging — so we couldn't see whether the cold-cache fetch was
running. Add an optional `tag` parameter that, when set, debugPrints
each step (drift hit/miss, connectivity, fetch start/done/error).
Wire tags into artistProvider and artistAlbumsProvider, plus add the
3s connectivity timeout there too as belt-and-suspenders.

Next test pass should show cacheFirst[artist(<id>)]: lines that
pinpoint where the artist screen is sticking.
This commit is contained in:
2026-05-11 08:35:02 -04:00
parent 3c0806d8fd
commit f88e82a777
4 changed files with 47 additions and 9 deletions
@@ -6,6 +6,7 @@ import '../cache/audio_cache_manager.dart';
import '../cache/db.dart';
import '../likes/like_button.dart';
import '../player/player_provider.dart';
import '../shared/widgets/server_image.dart';
import '../theme/theme_extension.dart';
import 'library_providers.dart';
import 'widgets/track_row.dart';
@@ -27,7 +28,18 @@ class AlbumDetailScreen extends ConsumerWidget {
Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [
Container(width: 96, height: 96, color: fs.slate),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
width: 96,
height: 96,
child: ServerImage(
url: '/api/albums/$id/cover',
fit: BoxFit.cover,
fallback: Container(color: fs.slate),
),
),
),
const SizedBox(width: 16),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -6,6 +6,7 @@ import '../api/endpoints/likes.dart';
import '../likes/like_button.dart';
import '../models/album.dart';
import '../player/player_provider.dart';
import '../shared/widgets/server_image.dart';
import '../theme/theme_extension.dart';
import 'library_providers.dart';
import 'widgets/album_card.dart';
@@ -30,9 +31,18 @@ class ArtistDetailScreen extends ConsumerWidget {
Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [
Container(
width: 96, height: 96,
decoration: BoxDecoration(color: fs.slate, shape: BoxShape.circle),
ClipOval(
child: SizedBox(
width: 96,
height: 96,
child: a.coverUrl.isEmpty
? Container(color: fs.slate)
: ServerImage(
url: a.coverUrl,
fit: BoxFit.cover,
fallback: Container(color: fs.slate),
),
),
),
const SizedBox(width: 16),
Expanded(
@@ -61,8 +61,10 @@ final artistProvider =
toResult: (rows) => rows.isEmpty
? const ArtistRef(id: '', name: '')
: rows.first.toRef(),
isOnline: () async =>
(await ref.read(connectivityProvider.future)),
isOnline: () async => (await ref
.read(connectivityProvider.future)
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
tag: 'artist($id)',
);
});
@@ -90,8 +92,10 @@ final artistAlbumsProvider =
final artist = r.readTableOrNull(db.cachedArtists);
return album.toRef(artistName: artist?.name ?? '');
}).toList(),
isOnline: () async =>
(await ref.read(connectivityProvider.future)),
isOnline: () async => (await ref
.read(connectivityProvider.future)
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
tag: 'artistAlbums($artistId)',
);
});