- Now Playing shows where music plays and opens the phone's output switcher - Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal - Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app - 'Is this your song?' report form feeding takedown_requests (+ kind, user_id) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.0 KiB
Dart
137 lines
5.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../app/theme.dart';
|
|
import '../../core/env.dart';
|
|
import '../../audio/player_providers.dart';
|
|
import '../../core/models.dart';
|
|
import '../../data/user_library.dart';
|
|
import '../../shared/widgets/artwork.dart';
|
|
import '../../shared/widgets/catalog_builder.dart';
|
|
import '../../shared/widgets/section_header.dart';
|
|
import '../../shared/widgets/track_tile.dart';
|
|
|
|
enum _LibraryView { liked, recent, singers }
|
|
|
|
class LibraryScreen extends ConsumerStatefulWidget {
|
|
const LibraryScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<LibraryScreen> createState() => _LibraryScreenState();
|
|
}
|
|
|
|
class _LibraryScreenState extends ConsumerState<LibraryScreen> {
|
|
_LibraryView _view = _LibraryView.liked;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
bottom: false,
|
|
child: CatalogBuilder(builder: (catalog) {
|
|
final liked = ref.watch(likedSongsProvider);
|
|
final recent = ref.watch(recentlyPlayedProvider);
|
|
final controller = ref.read(playerControllerProvider);
|
|
|
|
List<Track> resolve(Iterable<String> ids) => ids.map(catalog.trackById).whereType<Track>().toList();
|
|
|
|
final Widget content = switch (_view) {
|
|
_LibraryView.liked => _TrackList(
|
|
tracks: resolve(liked),
|
|
empty: const EmptyState(
|
|
icon: Icons.favorite_border,
|
|
title: 'No liked songs yet',
|
|
message: 'Tap the heart on any song to keep it here.',
|
|
),
|
|
onPlay: controller.playTracks,
|
|
),
|
|
_LibraryView.recent => _TrackList(
|
|
tracks: resolve(recent),
|
|
empty: const EmptyState(icon: Icons.history, title: 'Nothing played yet'),
|
|
onPlay: controller.playTracks,
|
|
),
|
|
_LibraryView.singers => ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
children: [
|
|
for (final a in catalog.artists)
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: SingerAvatar(artist: a, size: 52),
|
|
title: Text(a.name, style: const TextStyle(fontWeight: FontWeight.w700)),
|
|
subtitle: Text('${catalog.byArtist(a.id).length} songs', style: const TextStyle(color: NkColors.muted)),
|
|
onTap: () => context.push('/artist/${a.id}'),
|
|
),
|
|
],
|
|
),
|
|
};
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(18, 12, 8, 12),
|
|
child: Row(
|
|
children: [
|
|
Expanded(child: Text('Your library', style: Theme.of(context).textTheme.headlineMedium)),
|
|
IconButton(
|
|
tooltip: 'Settings & help',
|
|
icon: const Icon(Icons.settings_outlined),
|
|
onPressed: () => context.push('/settings'),
|
|
),
|
|
if (Env.hasSupabase)
|
|
IconButton(
|
|
tooltip: 'Your profile',
|
|
icon: const Icon(Icons.account_circle_outlined),
|
|
onPressed: () => context.push('/profile'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
child: Row(
|
|
children: [
|
|
for (final (view, label) in const [
|
|
(_LibraryView.liked, 'Liked songs'),
|
|
(_LibraryView.recent, 'Recently played'),
|
|
(_LibraryView.singers, 'Singers'),
|
|
])
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: ChoiceChip(
|
|
label: Text(label),
|
|
selected: _view == view,
|
|
onSelected: (_) => setState(() => _view = view),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(child: content),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TrackList extends StatelessWidget {
|
|
const _TrackList({required this.tracks, required this.empty, required this.onPlay});
|
|
|
|
final List<Track> tracks;
|
|
final Widget empty;
|
|
final Future<void> Function(List<Track>, {int index, bool shuffle}) onPlay;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (tracks.isEmpty) return Center(child: SingleChildScrollView(child: empty));
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
itemCount: tracks.length,
|
|
itemBuilder: (_, i) => TrackTile(track: tracks[i], onTap: () => onPlay(tracks, index: i)),
|
|
);
|
|
}
|
|
}
|