fix(flutter): library Artists tab 404 + Albums tab 3-up grid

Artists tab 404: client called /api/library/artists, but the server
mounts the artists list at /api/artists (handleListArtists). Albums
sit at /api/library/albums for historical reasons — the paths aren't
symmetric. Switch listArtists() to /api/artists with sort=alpha.

Albums tab grid: matched the responsive 3-up layout we built for
artist detail. LayoutBuilder computes cellW from available width;
AlbumCard sized to the cell with titleMaxLines: 2; mainAxisExtent
matches actual content height (cover + 2-line title + artist line +
fudge). No more wide-aspect cells with empty space below the card.

Also wired `extra: ref` on the artists/albums grids and the Liked
tab so detail-screen nav hydration kicks in here too — taps from
library screens get the same instant header that home tiles do.
This commit is contained in:
2026-05-11 12:21:24 -04:00
parent 11c40c6aca
commit a77d4ceac0
2 changed files with 68 additions and 28 deletions
@@ -12,9 +12,16 @@ class LibraryListsApi {
final Dio _dio; final Dio _dio;
Future<Paged<ArtistRef>> listArtists({int limit = 50, int offset = 0}) async { Future<Paged<ArtistRef>> listArtists({int limit = 50, int offset = 0}) async {
// Server mounts the artists list at /api/artists (handleListArtists),
// not /api/library/artists. Albums use /api/library/albums for
// historical reasons; the paths aren't symmetric.
final r = await _dio.get<Map<String, dynamic>>( final r = await _dio.get<Map<String, dynamic>>(
'/api/library/artists', '/api/artists',
queryParameters: {'limit': limit, 'offset': offset}, queryParameters: {
'limit': limit,
'offset': offset,
'sort': 'alpha',
},
); );
return Paged.fromJson(r.data ?? const {}, ArtistRef.fromJson); return Paged.fromJson(r.data ?? const {}, ArtistRef.fromJson);
} }
+59 -26
View File
@@ -144,10 +144,14 @@ class _ArtistsTab extends ConsumerWidget {
childAspectRatio: 0.78, childAspectRatio: 0.78,
), ),
itemCount: page.items.length, itemCount: page.items.length,
itemBuilder: (ctx, i) => ArtistCard( itemBuilder: (ctx, i) {
artist: page.items[i], final artist = page.items[i];
onTap: () => ctx.push('/artists/${page.items[i].id}'), return ArtistCard(
), artist: artist,
onTap: () => ctx.push('/artists/${artist.id}',
extra: artist),
);
},
), ),
), ),
); );
@@ -167,20 +171,41 @@ class _AlbumsTab extends ConsumerWidget {
? 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 => ref.refresh(_libraryAlbumsProvider.future), onRefresh: () async => ref.refresh(_libraryAlbumsProvider.future),
child: GridView.builder( // Same responsive 3-up grid as the artist detail
padding: const EdgeInsets.all(8), // album list — LayoutBuilder + AlbumCard sized to the
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( // cell, mainAxisExtent matched to actual card height.
crossAxisCount: 2, child: LayoutBuilder(builder: (ctx, constraints) {
mainAxisSpacing: 8, const cols = 3;
crossAxisSpacing: 8, const sidePad = 8.0;
childAspectRatio: 0.78, const gap = 8.0;
), final cellW = (constraints.maxWidth -
itemCount: page.items.length, sidePad * 2 -
itemBuilder: (ctx, i) => AlbumCard( gap * (cols - 1)) /
album: page.items[i], cols;
onTap: () => ctx.push('/albums/${page.items[i].id}'), // cover (cellW - 16) + gap (8) + 2-line title (~36)
), // + artist line (~16) + small fudge.
), final cellH = (cellW - 16) + 8 + 36 + 16 + 4;
return GridView.builder(
padding: const EdgeInsets.all(sidePad),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: cols,
mainAxisExtent: cellH,
mainAxisSpacing: gap,
crossAxisSpacing: gap,
),
itemCount: page.items.length,
itemBuilder: (ctx, i) {
final album = page.items[i];
return AlbumCard(
album: album,
width: cellW,
titleMaxLines: 2,
onTap: () => ctx.push('/albums/${album.id}',
extra: album),
);
},
);
}),
), ),
); );
} }
@@ -260,10 +285,14 @@ class _LikedTab extends ConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: ar.items.length, itemCount: ar.items.length,
itemBuilder: (ctx, i) => ArtistCard( itemBuilder: (ctx, i) {
artist: ar.items[i], final artist = ar.items[i];
onTap: () => ctx.push('/artists/${ar.items[i].id}'), return ArtistCard(
), artist: artist,
onTap: () =>
ctx.push('/artists/${artist.id}', extra: artist),
);
},
), ),
), ),
], ],
@@ -275,10 +304,14 @@ class _LikedTab extends ConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: al.items.length, itemCount: al.items.length,
itemBuilder: (ctx, i) => AlbumCard( itemBuilder: (ctx, i) {
album: al.items[i], final album = al.items[i];
onTap: () => ctx.push('/albums/${al.items[i].id}'), return AlbumCard(
), album: album,
onTap: () =>
ctx.push('/albums/${album.id}', extra: album),
);
},
), ),
), ),
], ],