Files
SpotiFLAC-Mobile/lib/screens/search_screen.dart
T
zarzet 482457205a feat: Add Deezer as alternative metadata source with auto-fallback
- Add Deezer API client (no auth required, rate limit per user IP)
- Add search source selector in Settings (Deezer/Spotify)
- Default to Deezer for better reliability
- Auto-fallback to Deezer when Spotify API is rate limited (429)
- Support fallback for tracks and albums via SongLink API
- Update README with metadata source documentation
- Version 2.1.5-preview (build 42)
2026-01-07 03:25:14 +07:00

182 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:spotiflac_android/providers/track_provider.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
class SearchScreen extends ConsumerStatefulWidget {
final String query;
const SearchScreen({super.key, required this.query});
@override
ConsumerState<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
late TextEditingController _searchController;
@override
void initState() {
super.initState();
_searchController = TextEditingController(text: widget.query);
if (widget.query.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final settings = ref.read(settingsProvider);
ref.read(trackProvider.notifier).search(widget.query, metadataSource: settings.metadataSource);
});
}
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _search() {
final query = _searchController.text.trim();
if (query.isNotEmpty) {
final settings = ref.read(settingsProvider);
ref.read(trackProvider.notifier).search(query, metadataSource: settings.metadataSource);
}
}
void _downloadTrack(int index) {
final trackState = ref.read(trackProvider);
if (index >= 0 && index < trackState.tracks.length) {
final track = trackState.tracks[index];
final settings = ref.read(settingsProvider);
ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added "${track.name}" to queue')),
);
}
}
@override
Widget build(BuildContext context) {
final trackState = ref.watch(trackProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
style: TextStyle(color: colorScheme.onSurface),
decoration: InputDecoration(
hintText: 'Search tracks...',
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onSubmitted: (_) => _search(),
autofocus: widget.query.isEmpty,
),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: _search,
),
],
),
body: Column(
children: [
if (trackState.isLoading)
LinearProgressIndicator(color: colorScheme.primary),
if (trackState.error != null)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
trackState.error!,
style: TextStyle(color: colorScheme.error),
),
),
Expanded(
child: trackState.tracks.isEmpty
? _buildEmptyState(colorScheme)
: ListView.builder(
itemCount: trackState.tracks.length,
itemBuilder: (context, index) => _buildTrackTile(index, colorScheme),
),
),
],
),
);
}
Widget _buildEmptyState(ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'Search for tracks',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildTrackTile(int index, ColorScheme colorScheme) {
final track = ref.watch(trackProvider).tracks[index];
return ListTile(
leading: track.coverUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: track.coverUrl!,
width: 48,
height: 48,
fit: BoxFit.cover,
),
)
: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant),
),
title: Text(track.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
Text(
track.albumName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
trailing: IconButton(
icon: Icon(Icons.download, color: colorScheme.primary),
onPressed: () => _downloadTrack(index),
),
onTap: () => _downloadTrack(index),
);
}
}