feat(flutter): library screen with 5 tabs (Artists/Albums/History/Liked/Hidden)

Models:
- history_event.dart (HistoryEvent + HistoryPage envelope)
- quarantine_mine.dart (QuarantineMineRow for /api/quarantine/mine)
- Renamed Page<T> -> Paged<T> to avoid collision with Material's Page

Endpoints:
- library_lists.dart: listArtists / listAlbums (paged)
- me.dart: history + quarantineMine
- likes.dart: extended with listTracks/Albums/Artists (paged)

UI:
- library_screen.dart: TabBar over 5 tabs
- Artists tab: 3-col grid
- Albums tab: 2-col grid
- History tab: list with relative-time formatting (matches HistoryRow.svelte)
- Liked tab: 3 stacked sections (artists scroll-row, albums scroll-row, tracks list)
- Hidden tab: list of quarantined tracks with reason pill
- /library route + library_music icon on home AppBar

First page only for v1 (limit 50). Infinite scroll deferred.
This commit is contained in:
2026-05-08 14:40:26 -04:00
parent 5541171e94
commit 66545bf8c3
10 changed files with 636 additions and 12 deletions
@@ -14,20 +14,20 @@ class SearchResponse {
required this.tracks,
});
final Page<ArtistRef> artists;
final Page<AlbumRef> albums;
final Page<TrackRef> tracks;
final Paged<ArtistRef> artists;
final Paged<AlbumRef> albums;
final Paged<TrackRef> tracks;
factory SearchResponse.fromJson(Map<String, dynamic> j) => SearchResponse(
artists: Page.fromJson(
artists: Paged.fromJson(
(j['artists'] as Map?)?.cast<String, dynamic>() ?? const {},
ArtistRef.fromJson,
),
albums: Page.fromJson(
albums: Paged.fromJson(
(j['albums'] as Map?)?.cast<String, dynamic>() ?? const {},
AlbumRef.fromJson,
),
tracks: Page.fromJson(
tracks: Paged.fromJson(
(j['tracks'] as Map?)?.cast<String, dynamic>() ?? const {},
TrackRef.fromJson,
),