import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/music_queue_service.dart'; import '../services/spam_protection_service.dart'; import '../services/genre_filter_service.dart'; import '../models/spotify_track.dart'; import '../widgets/user_activity_status.dart'; class VotingPage extends StatefulWidget { const VotingPage({super.key}); @override State createState() => _VotingPageState(); } class _VotingPageState extends State { final TextEditingController _searchController = TextEditingController(); List _searchResults = []; bool _isLoading = false; String _statusMessage = ''; @override void initState() { super.initState(); _loadInitialQueue(); } Future _loadInitialQueue() async { final queueService = Provider.of(context, listen: false); await queueService.initializeQueue(); } Future _searchSpotify(String query) async { if (query.isEmpty) return; // Check if search query is appropriate if (!GenreFilterService.isSearchQueryAppropriate(query)) { final suggestions = GenreFilterService.getAlternativeSearchSuggestions(query); setState(() { _isLoading = false; _statusMessage = 'Search term not suitable for the peaceful Lido atmosphere. Try: ${suggestions.join(', ')}'; _searchResults = []; }); return; } final spamService = Provider.of(context, listen: false); final userId = spamService.getCurrentUserId(); // Check spam protection for suggestions if (!spamService.canSuggest(userId)) { final cooldown = spamService.getSuggestionCooldownRemaining(userId); final blockMessage = spamService.getBlockMessage(userId); setState(() { _isLoading = false; _statusMessage = blockMessage ?? 'Please wait $cooldown seconds before searching again.'; _searchResults = []; }); return; } setState(() { _isLoading = true; _statusMessage = 'Searching for "$query"...'; }); try { final queueService = Provider.of(context, listen: false); final results = await queueService.searchTracks(query); // Filter results based on genre appropriateness final filteredResults = GenreFilterService.filterSearchResults(results); // Record the suggestion attempt spamService.recordSuggestion(userId); setState(() { _searchResults = filteredResults; _isLoading = false; if (filteredResults.isEmpty && results.isNotEmpty) { _statusMessage = 'No tracks found that match the peaceful Lido atmosphere. Try searching for chill, ambient, or relaxing music.'; } else if (filteredResults.isEmpty) { _statusMessage = 'No tracks found for "$query"'; } else { final filtered = results.length - filteredResults.length; _statusMessage = 'Found ${filteredResults.length} tracks' + (filtered > 0 ? ' ($filtered filtered for atmosphere)' : ''); } }); } catch (e) { setState(() { _isLoading = false; _statusMessage = 'Search failed: ${e.toString()}'; _searchResults = []; }); } } void _addToQueue(SpotifyTrack track) { final spamService = Provider.of(context, listen: false); final userId = spamService.getCurrentUserId(); // Check if user can suggest (add to queue) if (!spamService.canSuggest(userId)) { final cooldown = spamService.getSuggestionCooldownRemaining(userId); final blockMessage = spamService.getBlockMessage(userId); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(blockMessage ?? 'Please wait $cooldown seconds before adding another song.'), duration: const Duration(seconds: 3), backgroundColor: Colors.orange, ), ); return; } // Check if track is appropriate for atmosphere final rejectionReason = GenreFilterService.getRejectionReason(track); if (rejectionReason != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(rejectionReason), duration: const Duration(seconds: 4), backgroundColor: Colors.orange, ), ); return; } final queueService = Provider.of(context, listen: false); queueService.addToQueue(track); // Record the suggestion spamService.recordSuggestion(userId); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Added "${track.name}" to queue'), duration: const Duration(seconds: 2), backgroundColor: const Color(0xFF6366F1), ), ); } @override Widget build(BuildContext context) { return Consumer( builder: (context, queueService, child) { return Scaffold( backgroundColor: const Color(0xFF121212), body: SafeArea( child: Column( children: [ // User Activity Status Consumer( builder: (context, spamService, child) { return UserActivityStatus(spamService: spamService); }, ), // Header with Search Container( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Voting', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), // Status indicator Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF6366F1).withOpacity(0.2), borderRadius: BorderRadius.circular(20), border: Border.all( color: const Color(0xFF6366F1), width: 1, ), ), child: const Text( '🎵 Spotify', style: TextStyle( color: Color(0xFF6366F1), fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 20), // Search Bar TextField( controller: _searchController, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'Search for songs, artists, albums...', hintStyle: const TextStyle(color: Colors.grey), prefixIcon: const Icon(Icons.search, color: Colors.grey), suffixIcon: _isLoading ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Color(0xFF6366F1)), ), ), ) : null, filled: true, fillColor: const Color(0xFF1E1E1E), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), onSubmitted: _searchSpotify, ), // Atmosphere Info Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF6366F1).withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF6366F1).withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.spa, color: Color(0xFF6366F1), size: 20), SizedBox(width: 8), Text( 'Lido Atmosphere', style: TextStyle( color: Color(0xFF6366F1), fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), const SizedBox(height: 8), Text( GenreFilterService.getAtmosphereDescription(), style: const TextStyle( color: Colors.grey, fontSize: 12, ), ), const SizedBox(height: 12), const Text( 'Try these searches:', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w500, fontSize: 12, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: GenreFilterService.getSuggestedSearchTerms() .take(8) .map((term) => InkWell( onTap: () { _searchController.text = term; _searchSpotify(term); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF6366F1).withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Text( term, style: const TextStyle( color: Color(0xFF6366F1), fontSize: 11, ), ), ), )) .toList(), ), ], ), ), // Status Message if (_statusMessage.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8), child: Text( _statusMessage, style: const TextStyle( color: Colors.grey, fontSize: 14, ), ), ), ], ), ), // Search Results and Queue Expanded( child: DefaultTabController( length: 2, child: Column( children: [ const TabBar( labelColor: Color(0xFF6366F1), unselectedLabelColor: Colors.grey, indicatorColor: Color(0xFF6366F1), tabs: [ Tab(text: 'Search Results'), Tab(text: 'Queue'), ], ), Expanded( child: TabBarView( children: [ // Search Results Tab _buildSearchResults(), // Queue Tab _buildQueueView(queueService), ], ), ), ], ), ), ), ], ), ), ); }, ); } Widget _buildSearchResults() { if (_searchResults.isEmpty && !_isLoading) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search, size: 80, color: Colors.grey, ), SizedBox(height: 20), Text( 'Search for songs to add to the queue', style: TextStyle( color: Colors.grey, fontSize: 16, ), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(20), itemCount: _searchResults.length, itemBuilder: (context, index) { final track = _searchResults[index]; return _buildTrackCard(track); }, ); } Widget _buildQueueView(MusicQueueService queueService) { final queue = queueService.queue; if (queue.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.queue_music, size: 80, color: Colors.grey, ), SizedBox(height: 20), Text( 'Queue is empty', style: TextStyle( color: Colors.grey, fontSize: 18, fontWeight: FontWeight.bold, ), ), SizedBox(height: 10), Text( 'Search and add songs to get started!', style: TextStyle( color: Colors.grey, fontSize: 16, ), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(20), itemCount: queue.length, itemBuilder: (context, index) { final queueItem = queue[index]; return _buildQueueItemCard(queueItem, index, queueService); }, ); } Widget _buildTrackCard(SpotifyTrack track) { return Card( color: const Color(0xFF1E1E1E), margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // Album Art Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: const Color(0xFF2A2A2A), ), child: track.album.images.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( track.album.images.first.url, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon( Icons.music_note, color: Colors.grey, ); }, ), ) : const Icon( Icons.music_note, color: Colors.grey, ), ), const SizedBox(width: 12), // Track Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( track.name, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( track.artists.map((a) => a.name).join(', '), style: const TextStyle( color: Colors.grey, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( track.album.name, style: const TextStyle( color: Colors.grey, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // Add Button IconButton( onPressed: () => _addToQueue(track), icon: const Icon( Icons.add_circle, color: Color(0xFF6366F1), size: 32, ), ), ], ), ), ); } Widget _buildQueueItemCard(QueueItem queueItem, int index, MusicQueueService queueService) { return Card( color: const Color(0xFF1E1E1E), margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // Position CircleAvatar( backgroundColor: const Color(0xFF6366F1), radius: 16, child: Text( '${index + 1}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ), ), const SizedBox(width: 12), // Album Art Container( width: 50, height: 50, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: const Color(0xFF2A2A2A), ), child: queueItem.track.album.images.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( queueItem.track.album.images.first.url, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon( Icons.music_note, color: Colors.grey, size: 20, ); }, ), ) : const Icon( Icons.music_note, color: Colors.grey, size: 20, ), ), const SizedBox(width: 12), // Track Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( queueItem.track.name, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( queueItem.track.artists.map((a) => a.name).join(', '), style: const TextStyle( color: Colors.grey, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // Voting Buttons Column( children: [ Consumer( builder: (context, spamService, child) { final userId = spamService.getCurrentUserId(); final canVote = spamService.canVote(userId); final cooldown = spamService.getVoteCooldownRemaining(userId); return IconButton( onPressed: canVote ? () { queueService.upvote(index); spamService.recordVote(userId); } : null, icon: Icon( Icons.keyboard_arrow_up, color: canVote ? Colors.green : Colors.grey, size: 28, ), tooltip: canVote ? 'Upvote' : 'Wait $cooldown seconds', ); } ), Text( '${queueItem.votes}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), Consumer( builder: (context, spamService, child) { final userId = spamService.getCurrentUserId(); final canVote = spamService.canVote(userId); final cooldown = spamService.getVoteCooldownRemaining(userId); return IconButton( onPressed: canVote ? () { queueService.downvote(index); spamService.recordVote(userId); } : null, icon: Icon( Icons.keyboard_arrow_down, color: canVote ? Colors.red : Colors.grey, size: 28, ), tooltip: canVote ? 'Downvote' : 'Wait $cooldown seconds', ); } ), ], ), ], ), ), ); } }