nkbmusic/lib/features/artist/artist_screen.dart
metatroncubeswdev 178d2d00ab Nanga Kathai: Flutter app, self-hosted backend and song tools
- Flutter app (Android/iOS): home, explore, albums, search, player with
  equalizer, mini player, sign up/sign in with email codes, profile,
  encrypted offline downloads, About page
- server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx
  (or Caddy), migrations, backups, email templates
- supabase/migrations: schema, row-level security, profiles
- tools/: song scanner and importer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:39:26 -04:00

137 lines
5.7 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/theme.dart';
import '../../audio/player_providers.dart';
import '../../core/models.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 ArtistScreen extends ConsumerWidget {
const ArtistScreen({super.key, required this.artistId});
final String artistId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
bottomNavigationBar: const SafeArea(child: MiniPlayer()),
body: CatalogBuilder(builder: (catalog) {
final artist = catalog.artistsById[artistId];
if (artist == null) {
return const SafeArea(
child: EmptyState(icon: Icons.person_off_outlined, title: 'Singer not found'),
);
}
final songs = catalog.byArtist(artistId);
final controller = ref.read(playerControllerProvider);
final subtitle = ['Singer', ...songs.map((s) => s.occasion.label).toSet(), ?artist.hatti].join(' · ');
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 320,
backgroundColor: NkColors.ground,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
if (artist.photoUrl != null)
CachedNetworkImage(imageUrl: artist.photoUrl!, fit: BoxFit.cover)
else
Image.asset('assets/images/nilgiris_tea.jpg', fit: BoxFit.cover, alignment: const Alignment(0, -0.2)),
const PhotoScrim(stops: [0.3, 1]),
if (artist.photoUrl == null)
Positioned(
right: 16,
top: MediaQuery.paddingOf(context).top + 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: NkColors.ground.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: NkColors.teaLeaf),
),
child: const Text('Singer photo pending', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
),
),
Positioned(
left: 20,
right: 20,
bottom: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(artist.name, style: Theme.of(context).textTheme.headlineLarge),
const SizedBox(height: 6),
Text(subtitle, style: const TextStyle(color: NkColors.mutedOnPhoto, fontSize: 15)),
],
),
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 8),
sliver: SliverToBoxAdapter(
child: Row(
children: [
Text('${songs.length} songs', style: const TextStyle(color: NkColors.muted)),
const Spacer(),
IconButton(
tooltip: 'Shuffle',
icon: const Icon(Icons.shuffle, color: NkColors.sun),
onPressed: songs.isEmpty ? null : () => controller.playTracks(songs, shuffle: true),
),
IconButton.filled(
tooltip: 'Play all',
style: IconButton.styleFrom(fixedSize: const Size(56, 56)),
onPressed: songs.isEmpty ? null : () => controller.playTracks(songs),
icon: const Icon(Icons.play_arrow_rounded, size: 30),
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),
sliver: SliverList.builder(
itemCount: songs.length,
itemBuilder: (_, i) => TrackTile(
track: songs[i],
subtitle: '${songs[i].occasion.label} · ${formatDuration(songs[i].duration)}',
onTap: () => controller.playTracks(songs, index: i),
),
),
),
if (artist.bio != null)
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: NkColors.surface, borderRadius: BorderRadius.circular(16)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('About', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 6),
Text(artist.bio!, style: const TextStyle(color: Color(0xFFC9D6CE), height: 1.5)),
],
),
),
),
),
],
);
}),
);
}
}