- 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>
122 lines
4.5 KiB
Dart
122 lines
4.5 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 '../../audio/player_providers.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 SearchScreen extends ConsumerStatefulWidget {
|
|
const SearchScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<SearchScreen> createState() => _SearchScreenState();
|
|
}
|
|
|
|
class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|
final _controller = TextEditingController();
|
|
String _query = '';
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
bottomNavigationBar: const SafeArea(child: MiniPlayer()),
|
|
appBar: AppBar(
|
|
backgroundColor: NkColors.ground,
|
|
titleSpacing: 0,
|
|
title: Padding(
|
|
padding: const EdgeInsets.only(right: 16),
|
|
child: TextField(
|
|
controller: _controller,
|
|
autofocus: true,
|
|
textInputAction: TextInputAction.search,
|
|
onChanged: (v) => setState(() => _query = v),
|
|
decoration: InputDecoration(
|
|
hintText: 'Songs, singers, Tamil or English',
|
|
prefixIcon: const Icon(Icons.search),
|
|
contentPadding: EdgeInsets.zero,
|
|
suffixIcon: _query.isEmpty
|
|
? null
|
|
: IconButton(
|
|
tooltip: 'Clear',
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => setState(() {
|
|
_controller.clear();
|
|
_query = '';
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: CatalogBuilder(builder: (catalog) {
|
|
if (_query.trim().isEmpty) {
|
|
return const EmptyState(
|
|
icon: Icons.manage_search,
|
|
title: 'Find any Badaga song',
|
|
message: 'Type in English letters, Tamil script or the English meaning. Spelling variations are fine.',
|
|
);
|
|
}
|
|
final songs = catalog.searchTracks(_query);
|
|
final singers = catalog.searchArtists(_query);
|
|
final albums = catalog.searchAlbums(_query);
|
|
if (songs.isEmpty && singers.isEmpty && albums.isEmpty) {
|
|
return EmptyState(
|
|
icon: Icons.search_off,
|
|
title: 'No results for "$_query"',
|
|
message: 'Try a shorter word or another spelling.',
|
|
);
|
|
}
|
|
final controller = ref.read(playerControllerProvider);
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(18, 8, 18, 24),
|
|
children: [
|
|
if (singers.isNotEmpty) ...[
|
|
const SectionHeader('Singers'),
|
|
for (final a in singers)
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: SingerAvatar(artist: a, size: 48),
|
|
title: Text(a.name, style: const TextStyle(fontWeight: FontWeight.w700)),
|
|
subtitle: Text(a.hatti ?? 'Singer', style: const TextStyle(color: NkColors.muted)),
|
|
onTap: () => context.push('/artist/${a.id}'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
if (albums.isNotEmpty) ...[
|
|
const SectionHeader('Albums'),
|
|
for (final a in albums)
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: SizedBox.square(dimension: 48, child: AlbumCover(album: a, size: 48)),
|
|
),
|
|
title: Text(a.title, style: const TextStyle(fontWeight: FontWeight.w700)),
|
|
subtitle: Text('${catalog.byAlbum(a.id).length} songs', style: const TextStyle(color: NkColors.muted)),
|
|
onTap: () => context.push('/album/${a.id}'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
if (songs.isNotEmpty) ...[
|
|
const SectionHeader('Songs'),
|
|
for (var i = 0; i < songs.length; i++)
|
|
TrackTile(track: songs[i], onTap: () => controller.playTracks(songs, index: i)),
|
|
],
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|