Files
swingmusic-extended/swingmusic_mobile/lib/features/search/search_screen.dart
T
Tomas Dvorak 9f1623bb34 Add swingmusic-mobile submodule to replace Android app
- Updated .gitmodules to include mobile app submodule
- Added swingmusic_mobile directory with Flutter app
- Mobile app will now be built in unified release workflow
2026-03-18 19:29:44 +01:00

523 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../core/widgets/album_card.dart';
import '../../core/widgets/track_list_tile.dart';
import '../../data/models/track_model.dart';
import '../../data/models/album_model.dart';
import '../../data/models/artist_model.dart' as artist_model;
import '../../data/models/search_suggestion_model.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> with TickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
// Search results
final List<TrackModel> _trackResults = [];
final List<AlbumModel> _albumResults = [];
final List<artist_model.ArtistModel> _artistResults = [];
bool _isSearching = false;
String _currentQuery = '';
// Search filters
String _selectedFilter = 'all'; // 'all', 'tracks', 'albums', 'artists'
final List<String> _searchFilters = ['all', 'tracks', 'albums', 'artists'];
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_searchFocusNode.requestFocus();
}
@override
void dispose() {
_tabController.dispose();
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Search Header
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// Search Bar
TextField(
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: _onSearchChanged,
onSubmitted: _onSearchSubmitted,
decoration: InputDecoration(
hintText: 'Search tracks, albums, artists...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: _clearSearch,
icon: const Icon(Icons.clear),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 12),
// Filter Chips
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _searchFilters.map((filter) {
final isSelected = _selectedFilter == filter;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(filter.toUpperCase()),
selected: isSelected,
onSelected: (selected) {
setState(() {
_selectedFilter = filter;
});
_performSearch(_currentQuery);
},
backgroundColor: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
labelStyle: TextStyle(
color: isSelected
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}).toList(),
),
),
],
),
),
// Search Results
Expanded(
child: _isSearching
? const Center(child: CircularProgressIndicator())
: _currentQuery.isEmpty
? _buildSearchSuggestions()
: _buildSearchResults(),
),
],
),
);
}
Widget _buildSearchSuggestions() {
return DefaultTabController(
length: 4,
child: Column(
children: [
TabBar(
controller: _tabController,
isScrollable: true,
tabs: const [
Tab(text: 'Top'),
Tab(text: 'Tracks'),
Tab(text: 'Albums'),
Tab(text: 'Artists'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildTopSearches(),
_buildRecentSearches(),
_buildBrowseGenres(),
_buildBrowseFolders(),
],
),
),
],
),
);
}
Widget _buildTopSearches() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Top Searches',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Sample top searches
...['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical'].map((genre) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
Icons.trending_up,
color: Theme.of(context).colorScheme.primary,
),
title: Text(genre),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
_searchController.text = genre;
_performSearch(genre);
},
),
);
}),
],
),
);
}
Widget _buildRecentSearches() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recent Searches',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Sample recent searches
...['Beatles', 'Queen', 'Pink Floyd', 'Led Zeppelin'].map((search) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
Icons.history,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
title: Text(search),
trailing: IconButton(
onPressed: () {
_searchController.text = search;
_performSearch(search);
},
icon: const Icon(Icons.arrow_forward_ios),
),
onTap: () {
_searchController.text = search;
_performSearch(search);
},
),
);
}),
],
),
);
}
Widget _buildBrowseGenres() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: ['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical', 'Hip Hop', 'Country', 'R&B'].length,
itemBuilder: (context, index) {
final genre = ['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical', 'Hip Hop', 'Country', 'R&B'][index];
return Card(
child: InkWell(
onTap: () {
_searchController.text = genre;
_performSearch(genre);
},
borderRadius: BorderRadius.circular(8),
child: Center(
child: Text(
genre,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
),
);
},
),
);
}
Widget _buildBrowseFolders() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'Folder browsing coming soon',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
Widget _buildSearchResults() {
if (_selectedFilter == 'all') {
return DefaultTabController(
length: 3,
child: Column(
children: [
TabBar(
tabs: const [
Tab(text: 'Tracks'),
Tab(text: 'Albums'),
Tab(text: 'Artists'),
],
),
Expanded(
child: TabBarView(
children: [
_buildTrackResults(),
_buildAlbumResults(),
_buildArtistResults(),
],
),
),
],
),
);
} else if (_selectedFilter == 'tracks') {
return _buildTrackResults();
} else if (_selectedFilter == 'albums') {
return _buildAlbumResults();
} else {
return _buildArtistResults();
}
}
Widget _buildTrackResults() {
if (_trackResults.isEmpty) {
return _buildEmptyResults('No tracks found');
}
return ListView.builder(
itemCount: _trackResults.length,
itemBuilder: (context, index) {
final track = _trackResults[index];
return TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
);
},
);
}
Widget _buildAlbumResults() {
if (_albumResults.isEmpty) {
return _buildEmptyResults('No albums found');
}
return GridView.builder(
padding: const EdgeInsets.all(16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _albumResults.length,
itemBuilder: (context, index) {
final album = _albumResults[index];
return AlbumCard(
album: album,
onTap: () {
// Navigate to album details
},
);
},
);
}
Widget _buildArtistResults() {
if (_artistResults.isEmpty) {
return _buildEmptyResults('No artists found');
}
return ListView.builder(
itemCount: _artistResults.length,
itemBuilder: (context, index) {
final artist = _artistResults[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
artist.name.isNotEmpty ? artist.name[0].toUpperCase() : '?',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
),
title: Text(artist.name),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Navigate to artist details
},
);
},
);
}
Widget _buildEmptyResults(String message) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Try different keywords or browse categories',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
void _onSearchChanged(String query) {
_currentQuery = query;
if (query.isEmpty) {
setState(() {
_isSearching = false;
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
} else {
// Debounce search
Future.delayed(const Duration(milliseconds: 500), () {
if (_searchController.text == query) {
_performSearch(query);
}
});
}
}
void _onSearchSubmitted(String query) {
_performSearch(query);
_searchFocusNode.unfocus();
}
void _clearSearch() {
_searchController.clear();
setState(() {
_currentQuery = '';
_isSearching = false;
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
}
Future<void> _performSearch(String query) async {
if (query.trim().isEmpty) return;
setState(() {
_isSearching = true;
});
try {
// Simulate API call
await Future.delayed(const Duration(milliseconds: 800));
// This would be actual API calls
setState(() {
_isSearching = false;
// For demo, just clear results
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
} catch (e) {
setState(() {
_isSearching = false;
});
// Handle error
}
}
void _playTrack(TrackModel track) {
final audioProvider = Provider.of<AudioProvider>(context, listen: false);
audioProvider.setQueue([track]);
audioProvider.loadTrack(track);
audioProvider.play();
Navigator.pushNamed(context, '/player');
}
}