Files
minstrel/flutter_client/lib/library/home_screen.dart
T
bvandeusen 147d6e280e feat(flutter): bump deps to latest + Search screen slice
Major version bumps (riverpod 2->3, go_router 14->17, just_audio 0.9->0.10,
flutter_secure_storage 9->10, google_fonts 6->8, flutter_lints 4->6).
package_info_plus held at 8.x due to win32 conflict with secure_storage 10.x.

Riverpod 3 breaks: AsyncValue.valueOrNull -> .value, StateProvider replaced
with Notifier subclass. just_audio 0.10: ConcatenatingAudioSource -> setAudioSources.

Search slice:
- models/page.dart (generic Page<T> envelope)
- models/search_response.dart (3-facet wrapper)
- api/endpoints/search.dart
- search/search_provider.dart (debounced 250ms)
- search/search_screen.dart (TextField + 3 horizontal/vertical sections)
- Wired /search route + search button on home AppBar
2026-05-08 14:30:01 -04:00

101 lines
3.4 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../api/errors.dart';
import '../models/album.dart';
import '../models/artist.dart';
import '../models/track.dart';
import '../player/player_provider.dart';
import '../shared/widgets/connection_error_banner.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,
appBar: AppBar(
backgroundColor: fs.obsidian,
elevation: 0,
title: Text('Minstrel', style: TextStyle(color: fs.parchment)),
actions: [
IconButton(
icon: Icon(Icons.search, color: fs.parchment),
tooltip: 'Search',
onPressed: () => context.push('/search'),
),
],
),
body: SafeArea(
child: ref.watch(homeProvider).when(
error: (e, _) {
final code = e is DioException ? ApiError.fromDio(e).code : 'unknown';
if (code == 'connection_refused') {
return ConnectionErrorBanner(
onRetry: () => ref.refresh(homeProvider),
);
}
return 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: () => ref.read(playerActionsProvider).playTracks([t]),
),
),
],
);
}