- 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>
175 lines
6.4 KiB
Dart
175 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../app/router.dart';
|
|
import '../../app/theme.dart';
|
|
import '../../audio/player_providers.dart';
|
|
import '../../core/models.dart';
|
|
import '../../offline/download_store.dart';
|
|
import '../../shared/widgets/artwork.dart';
|
|
import '../../shared/widgets/catalog_builder.dart';
|
|
import '../../shared/widgets/mini_player.dart';
|
|
import '../../shared/widgets/section_header.dart';
|
|
import '../../shared/widgets/track_tile.dart';
|
|
|
|
class AlbumScreen extends ConsumerWidget {
|
|
const AlbumScreen({super.key, required this.albumId});
|
|
|
|
final String albumId;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Scaffold(
|
|
bottomNavigationBar: const SafeArea(child: MiniPlayer()),
|
|
body: CatalogBuilder(builder: (catalog) {
|
|
final album = catalog.albumsById[albumId];
|
|
if (album == null) {
|
|
return const Center(
|
|
child: EmptyState(
|
|
icon: Icons.album_outlined,
|
|
title: 'Album not found',
|
|
message: 'It may have been removed.',
|
|
),
|
|
);
|
|
}
|
|
final tracks = catalog.byAlbum(albumId);
|
|
final total = tracks.fold(Duration.zero, (sum, t) => sum + t.duration);
|
|
final controller = ref.read(playerControllerProvider);
|
|
return CustomScrollView(
|
|
slivers: [
|
|
const SliverAppBar(pinned: true),
|
|
SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: SizedBox.square(dimension: 200, child: AlbumCover(album: album, size: 200)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Text(album.title,
|
|
textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineSmall),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
[
|
|
if (album.year != null) '${album.year}',
|
|
'${tracks.length} songs',
|
|
'${total.inMinutes} min',
|
|
].join(' · '),
|
|
style: const TextStyle(color: NkColors.muted),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(18, 12, 18, 8),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_DownloadAlbumButton(tracks: tracks),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
tooltip: 'Shuffle',
|
|
icon: const Icon(Icons.shuffle, color: NkColors.sun),
|
|
onPressed: tracks.isEmpty ? null : () => controller.playTracks(tracks, shuffle: true),
|
|
),
|
|
const SizedBox(width: 16),
|
|
IconButton.filled(
|
|
tooltip: 'Play all',
|
|
style: IconButton.styleFrom(fixedSize: const Size(56, 56)),
|
|
onPressed: tracks.isEmpty ? null : () => controller.playTracks(tracks),
|
|
icon: const Icon(Icons.play_arrow_rounded, size: 30),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
sliver: SliverList.builder(
|
|
itemCount: tracks.length,
|
|
itemBuilder: (_, i) => TrackTile(track: tracks[i], onTap: () => controller.playTracks(tracks, index: i)),
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DownloadAlbumButton extends ConsumerWidget {
|
|
const _DownloadAlbumButton({required this.tracks});
|
|
|
|
final List<Track> tracks;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final d = ref.watch(downloadsProvider);
|
|
final done = tracks.where((t) => d.isDownloaded(t.id)).length;
|
|
final busy = tracks.any((t) => d.progress.containsKey(t.id));
|
|
final all = tracks.isNotEmpty && done == tracks.length;
|
|
return IconButton(
|
|
tooltip: all ? 'Downloaded' : 'Download album',
|
|
icon: busy
|
|
? const SizedBox.square(dimension: 22, child: CircularProgressIndicator(strokeWidth: 2.5))
|
|
: Icon(all ? Icons.download_done_rounded : Icons.download_rounded,
|
|
color: all ? NkColors.teaLeaf : NkColors.cream),
|
|
onPressed: all || busy || tracks.isEmpty
|
|
? null
|
|
: () {
|
|
if (!isSignedIn) {
|
|
context.push('/login?mode=signup');
|
|
return;
|
|
}
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
ref.read(playerControllerProvider).download(tracks).then((problem) => messenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text(problem ?? 'Downloading ${tracks.length - done} songs. Find them in Downloads.'),
|
|
),
|
|
));
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Square album card for horizontal rows and grids.
|
|
class AlbumCard extends StatelessWidget {
|
|
const AlbumCard({super.key, required this.album, required this.songCount, required this.onTap, this.size = 140});
|
|
|
|
final Album album;
|
|
final int songCount;
|
|
final VoidCallback onTap;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(10),
|
|
onTap: onTap,
|
|
child: SizedBox(
|
|
width: size,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: SizedBox.square(dimension: size, child: AlbumCover(album: album, size: size)),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(album.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
Text('$songCount songs', style: const TextStyle(color: NkColors.muted, fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|