nkbmusic/lib/features/explore/occasion_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

90 lines
3.5 KiB
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/catalog_builder.dart';
import '../../shared/widgets/mini_player.dart';
import '../../shared/widgets/section_header.dart';
import '../../shared/widgets/track_tile.dart';
class OccasionScreen extends ConsumerWidget {
const OccasionScreen({super.key, required this.occasion});
final Occasion occasion;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
bottomNavigationBar: const SafeArea(child: MiniPlayer()),
body: CatalogBuilder(builder: (catalog) {
final tracks = catalog.byOccasion(occasion);
final controller = ref.read(playerControllerProvider);
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 200,
backgroundColor: occasion.color,
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsetsDirectional.only(start: 56, bottom: 16),
title: Text(occasion.label, style: Theme.of(context).textTheme.titleLarge),
background: ColoredBox(
color: occasion.color,
child: Align(
alignment: const Alignment(0.85, 0),
child: Icon(occasion.icon, size: 110, color: NkColors.cream.withValues(alpha: 0.25)),
),
),
),
),
if (tracks.isEmpty)
const SliverFillRemaining(
hasScrollBody: false,
child: EmptyState(
icon: Icons.library_music_outlined,
title: 'No songs here yet',
message: 'Songs for this occasion appear here once they are approved.',
),
)
else ...[
SliverPadding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 8),
sliver: SliverToBoxAdapter(
child: Row(
children: [
Expanded(
child: Text('${occasion.subtitle} · ${tracks.length} songs',
style: const TextStyle(color: NkColors.muted)),
),
IconButton(
tooltip: 'Shuffle',
icon: const Icon(Icons.shuffle, color: NkColors.sun),
onPressed: () => controller.playTracks(tracks, shuffle: true),
),
IconButton.filled(
tooltip: 'Play all',
style: IconButton.styleFrom(fixedSize: const Size(56, 56)),
onPressed: () => 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)),
),
),
],
],
);
}),
);
}
}