From 5ef782905e5f31285721c7aed2d6eca96a91bd15 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 2 May 2026 17:30:42 -0400 Subject: [PATCH] feat(flutter/home): home screen with five sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sections mirror the web home: Recently added · Rediscover (albums + artists) · Most played · Last played artists. Tap routes pushed for albums/artists; track-tap is wired in the player task. Pull-to-refresh re-fetches /api/home. --- flutter_client/lib/library/home_screen.dart | 73 +++++++++++++++++++ flutter_client/lib/shared/routing.dart | 9 +-- .../test/library/home_screen_test.dart | 36 +++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 flutter_client/lib/library/home_screen.dart create mode 100644 flutter_client/test/library/home_screen_test.dart diff --git a/flutter_client/lib/library/home_screen.dart b/flutter_client/lib/library/home_screen.dart new file mode 100644 index 00000000..dbffd213 --- /dev/null +++ b/flutter_client/lib/library/home_screen.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../models/album.dart'; +import '../models/artist.dart'; +import '../models/track.dart'; +import '../theme/theme_extension.dart'; +import 'library_providers.dart'; +import 'widgets/album_card.dart'; +import 'widgets/artist_card.dart'; +import 'widgets/horizontal_scroll_row.dart'; +import 'widgets/track_row.dart'; + +class HomeScreen extends ConsumerWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fs = Theme.of(context).extension()!; + return Scaffold( + backgroundColor: fs.obsidian, + body: SafeArea( + child: ref.watch(homeProvider).when( + error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))), + loading: () => const Center(child: CircularProgressIndicator()), + data: (h) => RefreshIndicator( + onRefresh: () async => ref.refresh(homeProvider.future), + child: ListView(children: [ + _albumsRow(context, 'Recently added', h.recentlyAddedAlbums), + _albumsRow(context, 'Rediscover', h.rediscoverAlbums), + _artistsRow(context, 'Rediscover artists', h.rediscoverArtists), + _tracksRow(context, ref, 'Most played', h.mostPlayedTracks), + _artistsRow(context, 'Last played artists', h.lastPlayedArtists), + const SizedBox(height: 96), + ]), + ), + ), + ), + ); + } + + Widget _albumsRow(BuildContext ctx, String title, List albums) => + HorizontalScrollRow( + title: title, + children: [ + for (final a in albums) + AlbumCard(album: a, onTap: () => ctx.push('/albums/${a.id}')), + ], + ); + + Widget _artistsRow(BuildContext ctx, String title, List artists) => + HorizontalScrollRow( + title: title, + children: [ + for (final a in artists) + ArtistCard(artist: a, onTap: () => ctx.push('/artists/${a.id}')), + ], + ); + + Widget _tracksRow(BuildContext ctx, WidgetRef ref, String title, List tracks) => + HorizontalScrollRow( + title: title, + height: 64, + children: [ + for (final t in tracks) + SizedBox( + width: 280, + child: TrackRow(track: t, onTap: () { /* play wired in Task 18 */ }), + ), + ], + ); +} diff --git a/flutter_client/lib/shared/routing.dart b/flutter_client/lib/shared/routing.dart index 5ad1b0c3..ac9ab4e2 100644 --- a/flutter_client/lib/shared/routing.dart +++ b/flutter_client/lib/shared/routing.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import '../auth/auth_provider.dart'; import '../auth/login_screen.dart'; import '../auth/server_url_screen.dart'; +import '../library/home_screen.dart'; import 'widgets/version_gate.dart'; GoRouter buildRouter(Ref ref) { @@ -30,7 +31,7 @@ GoRouter buildRouter(Ref ref) { ShellRoute( builder: (ctx, state, child) => VersionGate(child: _ShellWithPlayerBar(child: child)), routes: [ - GoRoute(path: '/home', builder: (_, __) => const _HomePlaceholder()), + GoRoute(path: '/home', builder: (_, __) => const HomeScreen()), ], ), ], @@ -51,9 +52,3 @@ class _ShellWithPlayerBar extends StatelessWidget { } } -class _HomePlaceholder extends StatelessWidget { - const _HomePlaceholder(); - @override - Widget build(BuildContext context) => - const Scaffold(body: Center(child: Text('Home — coming in Task 14'))); -} diff --git a/flutter_client/test/library/home_screen_test.dart b/flutter_client/test/library/home_screen_test.dart new file mode 100644 index 00000000..65b22ffd --- /dev/null +++ b/flutter_client/test/library/home_screen_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:minstrel/library/home_screen.dart'; +import 'package:minstrel/library/library_providers.dart'; +import 'package:minstrel/models/album.dart'; +import 'package:minstrel/models/artist.dart'; +import 'package:minstrel/models/home_data.dart'; +import 'package:minstrel/theme/theme_data.dart'; + +void main() { + testWidgets('home renders sections with rows', (tester) async { + final fixture = HomeData( + recentlyAddedAlbums: const [ + AlbumRef(id: 'a', title: 'Geogaddi', artistId: 'x', artistName: 'BoC'), + ], + rediscoverAlbums: const [], + rediscoverArtists: const [ + ArtistRef(id: 'r', name: 'Aphex Twin'), + ], + mostPlayedTracks: const [], + lastPlayedArtists: const [], + ); + await tester.pumpWidget(ProviderScope( + overrides: [ + homeProvider.overrideWith((ref) async => fixture), + ], + child: MaterialApp(theme: buildThemeData(), home: const HomeScreen()), + )); + await tester.pumpAndSettle(); + expect(find.text('Recently added'), findsOneWidget); + expect(find.text('Geogaddi'), findsOneWidget); + expect(find.text('Aphex Twin'), findsOneWidget); + }); +}