This commit is contained in:
Forrest 2026-07-29 14:14:18 -04:00
commit d205277cdd
182 changed files with 22978 additions and 0 deletions

View file

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../state/providers.dart';
import '../theme/tokens.dart';
import '../widgets/hairline_panel.dart';
import 'browser_screen.dart';
/// Home tab — Recently Played (album-art strip), Recent Tracks, and Rediscover,
/// all derived from the local play history (mirrors Ratune's home tab).
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(subsonicClientProvider);
final recentAlbums = ref.watch(recentAlbumsProvider);
final recentSongs = ref.watch(recentSongsProvider);
final rediscover = ref.watch(rediscoverProvider);
return ListView(
padding: const EdgeInsets.fromLTRB(
RatuneSpacing.lg,
RatuneSpacing.xl,
RatuneSpacing.lg,
RatuneSpacing.lg,
),
children: [
HairlinePanel(
title: 'Recently Played',
child: SizedBox(
height: 120,
child: recentAlbums.isEmpty
? const _Empty('No listening history yet.')
: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: recentAlbums.length,
separatorBuilder: (_, _) =>
const SizedBox(width: RatuneSpacing.md),
itemBuilder: (_, i) {
final rec = recentAlbums[i];
final art = (client != null && rec.coverArt != null)
? client.coverArtUri(rec.coverArt!, size: 240).toString()
: null;
return GestureDetector(
onTap: rec.albumId == null
? null
: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
AlbumScreen(id: rec.albumId!),
),
),
child: SizedBox(
width: 120,
child: ColoredBox(
color: RatuneColors.surface,
child: art != null
? Image.network(art,
fit: BoxFit.cover, gaplessPlayback: true)
: const Icon(Icons.album_outlined,
color: RatuneColors.dimmed),
),
),
);
},
),
),
),
const SizedBox(height: RatuneSpacing.xl),
HairlinePanel(
title: 'Recent Tracks',
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
child: recentSongs.isEmpty
? const Padding(
padding: EdgeInsets.all(RatuneSpacing.lg),
child: _Empty('Nothing played recently.'),
)
: Column(
children: [
for (final rec in recentSongs.take(8))
BrowseRow(
title: rec.title,
trailing: rec.artist,
onTap: () => ref
.read(playbackProvider.notifier)
.playSongs([rec.toSong()]),
onPlayNext: () => ref
.read(playbackProvider.notifier)
.playNext(rec.toSong()),
onAddToQueue: () => ref
.read(playbackProvider.notifier)
.addToQueue(rec.toSong()),
),
],
),
),
const SizedBox(height: RatuneSpacing.xl),
HairlinePanel(
title: 'Rediscover',
padding: const EdgeInsets.symmetric(vertical: RatuneSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (rediscover.isEmpty)
const Padding(
padding: EdgeInsets.all(RatuneSpacing.lg),
child: _Empty('Listen to more music to unlock suggestions.'),
)
else
for (final a in rediscover)
BrowseRow(
title: a.name,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ArtistScreen(id: a.artistId),
),
),
),
InkWell(
onTap: () =>
ref.read(rediscoverSeedProvider.notifier).state++,
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: RatuneSpacing.lg,
vertical: RatuneSpacing.md,
),
child: Text('↻ re-roll',
style: TextStyle(color: RatuneColors.dimmed)),
),
),
],
),
),
],
);
}
}
class _Empty extends StatelessWidget {
const _Empty(this.message);
final String message;
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.centerLeft,
child: Text(message, style: const TextStyle(color: RatuneColors.dimmed)),
);
}