feat(flutter/home): home screen with five sections
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.
This commit is contained in:
@@ -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<FabledSwordTheme>()!;
|
||||
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<AlbumRef> 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<ArtistRef> 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<TrackRef> 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 */ }),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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')));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user