added audio output for demo songs since spotify api doesnt support audio streaming
This commit is contained in:
parent
6b93f1206d
commit
a91654df03
14 changed files with 1261 additions and 77 deletions
|
@ -5,6 +5,8 @@ import 'pages/voting_page.dart';
|
|||
import 'pages/group_page.dart';
|
||||
import 'services/music_queue_service.dart';
|
||||
import 'services/network_group_service.dart';
|
||||
import 'services/spam_protection_service.dart';
|
||||
import 'services/audio_service.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
@ -20,6 +22,8 @@ class MyApp extends StatelessWidget {
|
|||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => MusicQueueService()),
|
||||
ChangeNotifierProvider(create: (context) => NetworkGroupService()),
|
||||
ChangeNotifierProvider(create: (context) => SpamProtectionService()),
|
||||
ChangeNotifierProvider(create: (context) => AudioService()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'SleepySound',
|
||||
|
@ -81,10 +85,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(_getPageTitle()),
|
||||
),
|
||||
body: _getSelectedPage(),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
|
@ -110,17 +110,4 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getPageTitle() {
|
||||
switch (_selectedIndex) {
|
||||
case 0:
|
||||
return 'Now Playing';
|
||||
case 1:
|
||||
return 'Voting';
|
||||
case 2:
|
||||
return 'Group';
|
||||
default:
|
||||
return 'Now Playing';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
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});
|
||||
|
@ -30,6 +33,33 @@ class _VotingPageState extends State<VotingPage> {
|
|||
Future<void> _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<SpamProtectionService>(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"...';
|
||||
|
@ -39,12 +69,24 @@ class _VotingPageState extends State<VotingPage> {
|
|||
final queueService = Provider.of<MusicQueueService>(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 = results;
|
||||
_searchResults = filteredResults;
|
||||
_isLoading = false;
|
||||
_statusMessage = results.isEmpty
|
||||
? 'No tracks found for "$query"'
|
||||
: 'Found ${results.length} tracks';
|
||||
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(() {
|
||||
|
@ -56,9 +98,43 @@ class _VotingPageState extends State<VotingPage> {
|
|||
}
|
||||
|
||||
void _addToQueue(SpotifyTrack track) {
|
||||
final spamService = Provider.of<SpamProtectionService>(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<MusicQueueService>(context, listen: false);
|
||||
queueService.addToQueue(track);
|
||||
|
||||
// Record the suggestion
|
||||
spamService.recordSuggestion(userId);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Added "${track.name}" to queue'),
|
||||
|
@ -77,6 +153,13 @@ class _VotingPageState extends State<VotingPage> {
|
|||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// User Activity Status
|
||||
Consumer<SpamProtectionService>(
|
||||
builder: (context, spamService, child) {
|
||||
return UserActivityStatus(spamService: spamService);
|
||||
},
|
||||
),
|
||||
|
||||
// Header with Search
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
@ -149,6 +232,81 @@ class _VotingPageState extends State<VotingPage> {
|
|||
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(
|
||||
|
@ -458,13 +616,25 @@ class _VotingPageState extends State<VotingPage> {
|
|||
// Voting Buttons
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => queueService.upvote(index),
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Colors.green,
|
||||
size: 28,
|
||||
),
|
||||
Consumer<SpamProtectionService>(
|
||||
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}',
|
||||
|
@ -473,13 +643,25 @@ class _VotingPageState extends State<VotingPage> {
|
|||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => queueService.downvote(index),
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Colors.red,
|
||||
size: 28,
|
||||
),
|
||||
Consumer<SpamProtectionService>(
|
||||
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',
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
242
CHALLENGE_2/sleepysound/lib/services/audio_service.dart
Normal file
242
CHALLENGE_2/sleepysound/lib/services/audio_service.dart
Normal file
|
@ -0,0 +1,242 @@
|
|||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/spotify_track.dart';
|
||||
|
||||
class AudioService extends ChangeNotifier {
|
||||
static final AudioService _instance = AudioService._internal();
|
||||
factory AudioService() => _instance;
|
||||
AudioService._internal() {
|
||||
_initializePlayer();
|
||||
}
|
||||
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
// Current track state
|
||||
SpotifyTrack? _currentTrack;
|
||||
bool _isPlaying = false;
|
||||
bool _isLoading = false;
|
||||
Duration _currentPosition = Duration.zero;
|
||||
Duration _totalDuration = Duration.zero;
|
||||
|
||||
// Getters
|
||||
SpotifyTrack? get currentTrack => _currentTrack;
|
||||
bool get isPlaying => _isPlaying;
|
||||
bool get isLoading => _isLoading;
|
||||
Duration get currentPosition => _currentPosition;
|
||||
Duration get totalDuration => _totalDuration;
|
||||
double get progress => _totalDuration.inMilliseconds > 0
|
||||
? _currentPosition.inMilliseconds / _totalDuration.inMilliseconds
|
||||
: 0.0;
|
||||
|
||||
// Free audio sources for demo purposes
|
||||
// Using royalty-free music from reliable sources
|
||||
final Map<String, String> _demoAudioUrls = {
|
||||
// Peaceful, lido-appropriate tracks
|
||||
'pop1': 'https://www.bensound.com/bensound-music/bensound-relaxing.mp3',
|
||||
'pop2': 'https://www.bensound.com/bensound-music/bensound-sunny.mp3',
|
||||
'pop3': 'https://www.bensound.com/bensound-music/bensound-jazzcomedy.mp3',
|
||||
'pop4': 'https://www.bensound.com/bensound-music/bensound-acousticbreeze.mp3',
|
||||
'1': 'https://www.bensound.com/bensound-music/bensound-creativeminds.mp3',
|
||||
'2': 'https://www.bensound.com/bensound-music/bensound-happyrock.mp3',
|
||||
'3': 'https://www.bensound.com/bensound-music/bensound-ukulele.mp3',
|
||||
'4': 'https://www.bensound.com/bensound-music/bensound-summer.mp3',
|
||||
'5': 'https://www.bensound.com/bensound-music/bensound-happiness.mp3',
|
||||
};
|
||||
|
||||
void _initializePlayer() {
|
||||
// Listen to player state changes
|
||||
_audioPlayer.onPlayerStateChanged.listen((PlayerState state) {
|
||||
_isPlaying = state == PlayerState.playing;
|
||||
_isLoading = state == PlayerState.stopped && _currentTrack != null;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
// Listen to position changes
|
||||
_audioPlayer.onPositionChanged.listen((Duration position) {
|
||||
_currentPosition = position;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
// Listen to duration changes
|
||||
_audioPlayer.onDurationChanged.listen((Duration duration) {
|
||||
_totalDuration = duration;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
// Listen for track completion
|
||||
_audioPlayer.onPlayerComplete.listen((_) {
|
||||
_onTrackComplete();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> playTrack(SpotifyTrack track) async {
|
||||
try {
|
||||
_isLoading = true;
|
||||
_currentTrack = track;
|
||||
notifyListeners();
|
||||
|
||||
// Check if we have a demo URL for this track
|
||||
String? audioUrl = _demoAudioUrls[track.id];
|
||||
|
||||
if (audioUrl != null) {
|
||||
// Play the demo audio
|
||||
await _audioPlayer.play(UrlSource(audioUrl));
|
||||
print('Playing audio for: ${track.name} by ${track.artistNames}');
|
||||
} else {
|
||||
// For tracks without demo URLs, simulate playback
|
||||
print('Simulating playback for: ${track.name} by ${track.artistNames}');
|
||||
_simulateTrackPlayback(track);
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('Error playing track: $e');
|
||||
_isLoading = false;
|
||||
// Fallback to simulation
|
||||
_simulateTrackPlayback(track);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _simulateTrackPlayback(SpotifyTrack track) {
|
||||
// Set simulated duration
|
||||
_totalDuration = Duration(milliseconds: track.durationMs);
|
||||
_currentPosition = Duration.zero;
|
||||
_isPlaying = true;
|
||||
|
||||
// Simulate playback progress
|
||||
_startSimulatedProgress();
|
||||
}
|
||||
|
||||
void _startSimulatedProgress() {
|
||||
if (_isPlaying && _currentTrack != null) {
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_isPlaying && _currentTrack != null) {
|
||||
_currentPosition = _currentPosition + const Duration(seconds: 1);
|
||||
|
||||
if (_currentPosition >= _totalDuration) {
|
||||
_onTrackComplete();
|
||||
} else {
|
||||
notifyListeners();
|
||||
_startSimulatedProgress();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> togglePlayPause() async {
|
||||
try {
|
||||
if (_isPlaying) {
|
||||
await _audioPlayer.pause();
|
||||
} else {
|
||||
if (_currentTrack != null) {
|
||||
// Check if we have a real audio URL
|
||||
String? audioUrl = _demoAudioUrls[_currentTrack!.id];
|
||||
if (audioUrl != null) {
|
||||
await _audioPlayer.resume();
|
||||
} else {
|
||||
// Resume simulation
|
||||
_isPlaying = true;
|
||||
_startSimulatedProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('Error toggling play/pause: $e');
|
||||
// Fallback to simulation toggle
|
||||
_isPlaying = !_isPlaying;
|
||||
if (_isPlaying) {
|
||||
_startSimulatedProgress();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
await _audioPlayer.stop();
|
||||
} catch (e) {
|
||||
print('Error stopping audio: $e');
|
||||
}
|
||||
|
||||
_isPlaying = false;
|
||||
_currentPosition = Duration.zero;
|
||||
_currentTrack = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> seekTo(Duration position) async {
|
||||
try {
|
||||
// Check if we have a real audio URL
|
||||
if (_currentTrack != null && _demoAudioUrls.containsKey(_currentTrack!.id)) {
|
||||
await _audioPlayer.seek(position);
|
||||
} else {
|
||||
// Simulate seeking
|
||||
_currentPosition = position;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error seeking: $e');
|
||||
// Fallback to simulation
|
||||
_currentPosition = position;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTrackComplete() {
|
||||
_isPlaying = false;
|
||||
_currentPosition = Duration.zero;
|
||||
notifyListeners();
|
||||
|
||||
// Notify that track is complete (for queue management)
|
||||
onTrackComplete?.call();
|
||||
}
|
||||
|
||||
// Callback for when a track completes
|
||||
Function()? onTrackComplete;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Get formatted time strings
|
||||
String get currentPositionString => _formatDuration(_currentPosition);
|
||||
String get totalDurationString => _formatDuration(_totalDuration);
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return '$twoDigitMinutes:$twoDigitSeconds';
|
||||
}
|
||||
|
||||
// Add better demo audio URLs (using royalty-free sources)
|
||||
void addDemoAudioUrl(String trackId, String audioUrl) {
|
||||
_demoAudioUrls[trackId] = audioUrl;
|
||||
}
|
||||
|
||||
// Add local asset support
|
||||
Future<void> playAsset(SpotifyTrack track, String assetPath) async {
|
||||
try {
|
||||
_isLoading = true;
|
||||
_currentTrack = track;
|
||||
notifyListeners();
|
||||
|
||||
await _audioPlayer.play(AssetSource(assetPath));
|
||||
print('Playing asset: $assetPath for ${track.name}');
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('Error playing asset: $e');
|
||||
_isLoading = false;
|
||||
_simulateTrackPlayback(track);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
288
CHALLENGE_2/sleepysound/lib/services/genre_filter_service.dart
Normal file
288
CHALLENGE_2/sleepysound/lib/services/genre_filter_service.dart
Normal file
|
@ -0,0 +1,288 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import '../models/spotify_track.dart';
|
||||
|
||||
class GenreFilterService {
|
||||
// Allowed genres for the Lido atmosphere (chill, ambient, relaxing)
|
||||
static const List<String> allowedGenres = [
|
||||
// Chill and Ambient
|
||||
'chill',
|
||||
'chillout',
|
||||
'ambient',
|
||||
'new age',
|
||||
'meditation',
|
||||
'nature sounds',
|
||||
'spa',
|
||||
'yoga',
|
||||
|
||||
// Smooth genres
|
||||
'smooth jazz',
|
||||
'neo soul',
|
||||
'downtempo',
|
||||
'trip hop',
|
||||
'lo-fi',
|
||||
'lo-fi hip hop',
|
||||
'chillwave',
|
||||
'synthwave',
|
||||
|
||||
// Acoustic and Folk
|
||||
'acoustic',
|
||||
'folk',
|
||||
'indie folk',
|
||||
'singer-songwriter',
|
||||
'soft rock',
|
||||
'alternative',
|
||||
'indie',
|
||||
|
||||
// World and Cultural
|
||||
'world music',
|
||||
'bossa nova',
|
||||
'latin',
|
||||
'reggae',
|
||||
'dub',
|
||||
'tropical',
|
||||
'caribbean',
|
||||
|
||||
// Electronic (chill variants)
|
||||
'house',
|
||||
'deep house',
|
||||
'minimal techno',
|
||||
'ambient techno',
|
||||
'electronica',
|
||||
'minimal',
|
||||
|
||||
// Classical and Instrumental
|
||||
'classical',
|
||||
'instrumental',
|
||||
'piano',
|
||||
'string quartet',
|
||||
'chamber music',
|
||||
'contemporary classical',
|
||||
];
|
||||
|
||||
// Explicitly blocked genres (too energetic/aggressive for Lido)
|
||||
static const List<String> blockedGenres = [
|
||||
'metal',
|
||||
'death metal',
|
||||
'black metal',
|
||||
'hardcore',
|
||||
'punk',
|
||||
'hardcore punk',
|
||||
'grindcore',
|
||||
'screamo',
|
||||
'dubstep',
|
||||
'drum and bass',
|
||||
'breakcore',
|
||||
'speedcore',
|
||||
'gabber',
|
||||
'hardstyle',
|
||||
'hard trance',
|
||||
'psytrance',
|
||||
'hard rock',
|
||||
'thrash',
|
||||
'noise',
|
||||
'industrial',
|
||||
'aggressive',
|
||||
'rap',
|
||||
'hip hop',
|
||||
'trap',
|
||||
'drill',
|
||||
'grime',
|
||||
'gangsta rap',
|
||||
];
|
||||
|
||||
// Keywords that suggest inappropriate content
|
||||
static const List<String> blockedKeywords = [
|
||||
'explicit',
|
||||
'party',
|
||||
'club',
|
||||
'rave',
|
||||
'aggressive',
|
||||
'angry',
|
||||
'violent',
|
||||
'loud',
|
||||
'hardcore',
|
||||
'extreme',
|
||||
'intense',
|
||||
'heavy',
|
||||
'wild',
|
||||
'crazy',
|
||||
'insane',
|
||||
'brutal',
|
||||
'savage',
|
||||
'beast',
|
||||
'fire',
|
||||
'lit',
|
||||
'banger',
|
||||
'drop',
|
||||
'bass drop',
|
||||
'festival',
|
||||
'mosh',
|
||||
'headbang',
|
||||
];
|
||||
|
||||
// Check if a track is appropriate for the Lido atmosphere
|
||||
static bool isTrackAllowed(SpotifyTrack track) {
|
||||
final trackName = track.name.toLowerCase();
|
||||
final artistNames = track.artists.map((a) => a.name.toLowerCase()).join(' ');
|
||||
final albumName = track.album.name.toLowerCase();
|
||||
|
||||
// Check for blocked keywords in track, artist, or album names
|
||||
for (final keyword in blockedKeywords) {
|
||||
if (trackName.contains(keyword) ||
|
||||
artistNames.contains(keyword) ||
|
||||
albumName.contains(keyword)) {
|
||||
if (kDebugMode) {
|
||||
print('Track blocked due to keyword: $keyword');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For now, we'll allow tracks unless they contain blocked keywords
|
||||
// In a real implementation, you'd check against Spotify's genre data
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if a genre is allowed
|
||||
static bool isGenreAllowed(String genre) {
|
||||
final lowerGenre = genre.toLowerCase();
|
||||
|
||||
// Check if explicitly blocked
|
||||
if (blockedGenres.contains(lowerGenre)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if explicitly allowed
|
||||
if (allowedGenres.contains(lowerGenre)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for partial matches in allowed genres
|
||||
for (final allowedGenre in allowedGenres) {
|
||||
if (lowerGenre.contains(allowedGenre) || allowedGenre.contains(lowerGenre)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for partial matches in blocked genres
|
||||
for (final blockedGenre in blockedGenres) {
|
||||
if (lowerGenre.contains(blockedGenre) || blockedGenre.contains(lowerGenre)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to allowed if not explicitly blocked
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get suggested search terms for the atmosphere
|
||||
static List<String> getSuggestedSearchTerms() {
|
||||
return [
|
||||
'chill',
|
||||
'ambient',
|
||||
'acoustic',
|
||||
'coffee shop',
|
||||
'study music',
|
||||
'relaxing',
|
||||
'peaceful',
|
||||
'smooth',
|
||||
'sunset',
|
||||
'ocean',
|
||||
'nature',
|
||||
'meditation',
|
||||
'spa music',
|
||||
'lo-fi',
|
||||
'bossa nova',
|
||||
'jazz',
|
||||
'instrumental',
|
||||
'piano',
|
||||
'guitar',
|
||||
'folk',
|
||||
'indie',
|
||||
'world music',
|
||||
'downtempo',
|
||||
'chillout',
|
||||
'lounge',
|
||||
'soft rock',
|
||||
];
|
||||
}
|
||||
|
||||
// Get genre description for users
|
||||
static String getAtmosphereDescription() {
|
||||
return 'To maintain the peaceful Lido atmosphere, we feature chill, ambient, and relaxing music. Think coffee shop vibes, sunset sounds, and music that enhances tranquility.';
|
||||
}
|
||||
|
||||
// Filter search results based on allowed genres
|
||||
static List<SpotifyTrack> filterSearchResults(List<SpotifyTrack> tracks) {
|
||||
return tracks.where((track) => isTrackAllowed(track)).toList();
|
||||
}
|
||||
|
||||
// Get reason why a track might be rejected
|
||||
static String? getRejectionReason(SpotifyTrack track) {
|
||||
final trackName = track.name.toLowerCase();
|
||||
final artistNames = track.artists.map((a) => a.name.toLowerCase()).join(' ');
|
||||
final albumName = track.album.name.toLowerCase();
|
||||
|
||||
// Check for blocked keywords
|
||||
for (final keyword in blockedKeywords) {
|
||||
if (trackName.contains(keyword) ||
|
||||
artistNames.contains(keyword) ||
|
||||
albumName.contains(keyword)) {
|
||||
return 'This track contains content that might disturb the peaceful Lido atmosphere. Try searching for more chill or ambient music.';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if search query suggests inappropriate content
|
||||
static bool isSearchQueryAppropriate(String query) {
|
||||
final lowerQuery = query.toLowerCase();
|
||||
|
||||
for (final keyword in blockedKeywords) {
|
||||
if (lowerQuery.contains(keyword)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (final genre in blockedGenres) {
|
||||
if (lowerQuery.contains(genre)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get alternative search suggestions for inappropriate queries
|
||||
static List<String> getAlternativeSearchSuggestions(String inappropriateQuery) {
|
||||
// Map inappropriate terms to chill alternatives
|
||||
final alternatives = {
|
||||
'party': ['chill', 'lounge', 'relaxing'],
|
||||
'club': ['ambient', 'downtempo', 'smooth'],
|
||||
'rave': ['meditation', 'spa music', 'nature sounds'],
|
||||
'metal': ['acoustic', 'folk', 'classical'],
|
||||
'punk': ['indie', 'alternative', 'soft rock'],
|
||||
'hardcore': ['peaceful', 'calming', 'serene'],
|
||||
'aggressive': ['gentle', 'soothing', 'mellow'],
|
||||
'loud': ['quiet', 'soft', 'whisper'],
|
||||
'heavy': ['light', 'airy', 'floating'],
|
||||
'intense': ['relaxed', 'easy', 'laid-back'],
|
||||
};
|
||||
|
||||
final suggestions = <String>[];
|
||||
final lowerQuery = inappropriateQuery.toLowerCase();
|
||||
|
||||
for (final entry in alternatives.entries) {
|
||||
if (lowerQuery.contains(entry.key)) {
|
||||
suggestions.addAll(entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.isEmpty) {
|
||||
suggestions.addAll(['chill', 'ambient', 'relaxing', 'peaceful']);
|
||||
}
|
||||
|
||||
return suggestions.take(3).toList();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import '../models/spotify_track.dart';
|
||||
import '../services/spotify_service.dart';
|
||||
import '../services/audio_service.dart';
|
||||
|
||||
class QueueItem {
|
||||
final SpotifyTrack track;
|
||||
|
@ -33,11 +34,10 @@ class MusicQueueService extends ChangeNotifier {
|
|||
MusicQueueService._internal();
|
||||
|
||||
final SpotifyService _spotifyService = SpotifyService();
|
||||
final AudioService _audioService = AudioService();
|
||||
|
||||
// Current playing track
|
||||
SpotifyTrack? _currentTrack;
|
||||
bool _isPlaying = false;
|
||||
double _progress = 0.0;
|
||||
|
||||
// Queue management
|
||||
final List<QueueItem> _queue = [];
|
||||
|
@ -46,9 +46,9 @@ class MusicQueueService extends ChangeNotifier {
|
|||
final List<SpotifyTrack> _recentlyPlayed = [];
|
||||
|
||||
// Getters
|
||||
SpotifyTrack? get currentTrack => _currentTrack;
|
||||
bool get isPlaying => _isPlaying;
|
||||
double get progress => _progress;
|
||||
SpotifyTrack? get currentTrack => _audioService.currentTrack ?? _currentTrack;
|
||||
bool get isPlaying => _audioService.isPlaying;
|
||||
double get progress => _audioService.progress;
|
||||
List<QueueItem> get queue => List.unmodifiable(_queue);
|
||||
List<SpotifyTrack> get recentlyPlayed => List.unmodifiable(_recentlyPlayed);
|
||||
|
||||
|
@ -102,13 +102,14 @@ class MusicQueueService extends ChangeNotifier {
|
|||
});
|
||||
}
|
||||
|
||||
// Playback simulation
|
||||
void playNext() {
|
||||
// Playback control
|
||||
Future<void> playNext() async {
|
||||
if (_queue.isNotEmpty) {
|
||||
final nextItem = _queue.removeAt(0);
|
||||
_currentTrack = nextItem.track;
|
||||
_isPlaying = true;
|
||||
_progress = 0.0;
|
||||
|
||||
// Use audio service to actually play the track
|
||||
await _audioService.playTrack(nextItem.track);
|
||||
|
||||
// Add to recently played
|
||||
_recentlyPlayed.insert(0, nextItem.track);
|
||||
|
@ -118,37 +119,25 @@ class MusicQueueService extends ChangeNotifier {
|
|||
|
||||
notifyListeners();
|
||||
print('Now playing: ${_currentTrack!.name} by ${_currentTrack!.artistNames}');
|
||||
|
||||
// Simulate track progress
|
||||
_simulatePlayback();
|
||||
}
|
||||
}
|
||||
|
||||
void togglePlayPause() {
|
||||
_isPlaying = !_isPlaying;
|
||||
Future<void> togglePlayPause() async {
|
||||
await _audioService.togglePlayPause();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void skipTrack() {
|
||||
playNext();
|
||||
Future<void> skipTrack() async {
|
||||
await playNext();
|
||||
}
|
||||
|
||||
void _simulatePlayback() {
|
||||
if (_currentTrack == null) return;
|
||||
|
||||
// Simulate track progress over time
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_isPlaying && _currentTrack != null) {
|
||||
_progress += 1.0 / (_currentTrack!.durationMs / 1000);
|
||||
if (_progress >= 1.0) {
|
||||
// Track finished, play next
|
||||
playNext();
|
||||
} else {
|
||||
notifyListeners();
|
||||
_simulatePlayback();
|
||||
}
|
||||
}
|
||||
});
|
||||
Future<void> seekTo(double position) async {
|
||||
if (_audioService.totalDuration != Duration.zero) {
|
||||
final seekPosition = Duration(
|
||||
milliseconds: (position * _audioService.totalDuration.inMilliseconds).round(),
|
||||
);
|
||||
await _audioService.seekTo(seekPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with some popular tracks
|
||||
|
@ -165,9 +154,14 @@ class MusicQueueService extends ChangeNotifier {
|
|||
_queue.add(queueItem);
|
||||
}
|
||||
|
||||
// Set up audio service callback for track completion
|
||||
_audioService.onTrackComplete = () {
|
||||
playNext();
|
||||
};
|
||||
|
||||
// Start playing the first track
|
||||
if (_queue.isNotEmpty) {
|
||||
playNext();
|
||||
await playNext();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
@ -189,16 +183,18 @@ class MusicQueueService extends ChangeNotifier {
|
|||
|
||||
// Get current track info for display
|
||||
Map<String, dynamic>? get currentTrackInfo {
|
||||
if (_currentTrack == null) return null;
|
||||
final track = currentTrack;
|
||||
if (track == null) return null;
|
||||
|
||||
return {
|
||||
'title': _currentTrack!.name,
|
||||
'artist': _currentTrack!.artistNames,
|
||||
'album': _currentTrack!.album.name,
|
||||
'imageUrl': _currentTrack!.imageUrl,
|
||||
'duration': _currentTrack!.duration,
|
||||
'progress': _progress,
|
||||
'isPlaying': _isPlaying,
|
||||
'title': track.name,
|
||||
'artist': track.artistNames,
|
||||
'album': track.album.name,
|
||||
'imageUrl': track.imageUrl,
|
||||
'duration': _audioService.totalDurationString,
|
||||
'currentTime': _audioService.currentPositionString,
|
||||
'progress': progress,
|
||||
'isPlaying': isPlaying,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SpamProtectionService extends ChangeNotifier {
|
||||
// Vote limits per user
|
||||
static const int maxVotesPerHour = 20;
|
||||
static const int maxVotesPerMinute = 5;
|
||||
static const int maxSuggestionsPerHour = 10;
|
||||
static const int maxSuggestionsPerMinute = 3;
|
||||
|
||||
// Cooldown periods (in seconds)
|
||||
static const int voteCooldown = 2;
|
||||
static const int suggestionCooldown = 10;
|
||||
|
||||
// User activity tracking
|
||||
final Map<String, List<DateTime>> _userVotes = {};
|
||||
final Map<String, List<DateTime>> _userSuggestions = {};
|
||||
final Map<String, DateTime> _lastVoteTime = {};
|
||||
final Map<String, DateTime> _lastSuggestionTime = {};
|
||||
final Map<String, int> _consecutiveActions = {};
|
||||
|
||||
// Blocked users (temporary)
|
||||
final Map<String, DateTime> _blockedUsers = {};
|
||||
|
||||
String getCurrentUserId() {
|
||||
// In a real app, this would come from authentication
|
||||
return 'current_user_${DateTime.now().millisecondsSinceEpoch ~/ 1000000}';
|
||||
}
|
||||
|
||||
// Check if user can vote
|
||||
bool canVote(String userId) {
|
||||
// Check if user is temporarily blocked
|
||||
if (_isUserBlocked(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if (_isOnCooldown(userId, _lastVoteTime, voteCooldown)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rate limits
|
||||
if (!_checkRateLimit(userId, _userVotes, maxVotesPerMinute, maxVotesPerHour)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user can suggest songs
|
||||
bool canSuggest(String userId) {
|
||||
// Check if user is temporarily blocked
|
||||
if (_isUserBlocked(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if (_isOnCooldown(userId, _lastSuggestionTime, suggestionCooldown)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rate limits
|
||||
if (!_checkRateLimit(userId, _userSuggestions, maxSuggestionsPerMinute, maxSuggestionsPerHour)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Record a vote action
|
||||
void recordVote(String userId) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Add to vote history
|
||||
_userVotes.putIfAbsent(userId, () => []).add(now);
|
||||
_lastVoteTime[userId] = now;
|
||||
|
||||
// Track consecutive actions for spam detection
|
||||
_incrementConsecutiveActions(userId);
|
||||
|
||||
// Clean old entries
|
||||
_cleanOldEntries(_userVotes[userId]!);
|
||||
|
||||
// Check for suspicious behavior
|
||||
_checkForSpam(userId);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Record a suggestion action
|
||||
void recordSuggestion(String userId) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Add to suggestion history
|
||||
_userSuggestions.putIfAbsent(userId, () => []).add(now);
|
||||
_lastSuggestionTime[userId] = now;
|
||||
|
||||
// Track consecutive actions for spam detection
|
||||
_incrementConsecutiveActions(userId);
|
||||
|
||||
// Clean old entries
|
||||
_cleanOldEntries(_userSuggestions[userId]!);
|
||||
|
||||
// Check for suspicious behavior
|
||||
_checkForSpam(userId);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Get remaining cooldown time in seconds
|
||||
int getVoteCooldownRemaining(String userId) {
|
||||
final lastVote = _lastVoteTime[userId];
|
||||
if (lastVote == null) return 0;
|
||||
|
||||
final elapsed = DateTime.now().difference(lastVote).inSeconds;
|
||||
return (voteCooldown - elapsed).clamp(0, voteCooldown);
|
||||
}
|
||||
|
||||
int getSuggestionCooldownRemaining(String userId) {
|
||||
final lastSuggestion = _lastSuggestionTime[userId];
|
||||
if (lastSuggestion == null) return 0;
|
||||
|
||||
final elapsed = DateTime.now().difference(lastSuggestion).inSeconds;
|
||||
return (suggestionCooldown - elapsed).clamp(0, suggestionCooldown);
|
||||
}
|
||||
|
||||
// Get user activity stats
|
||||
Map<String, int> getUserStats(String userId) {
|
||||
final now = DateTime.now();
|
||||
final hourAgo = now.subtract(const Duration(hours: 1));
|
||||
final minuteAgo = now.subtract(const Duration(minutes: 1));
|
||||
|
||||
final votesThisHour = _userVotes[userId]?.where((time) => time.isAfter(hourAgo)).length ?? 0;
|
||||
final votesThisMinute = _userVotes[userId]?.where((time) => time.isAfter(minuteAgo)).length ?? 0;
|
||||
final suggestionsThisHour = _userSuggestions[userId]?.where((time) => time.isAfter(hourAgo)).length ?? 0;
|
||||
final suggestionsThisMinute = _userSuggestions[userId]?.where((time) => time.isAfter(minuteAgo)).length ?? 0;
|
||||
|
||||
return {
|
||||
'votesThisHour': votesThisHour,
|
||||
'votesThisMinute': votesThisMinute,
|
||||
'suggestionsThisHour': suggestionsThisHour,
|
||||
'suggestionsThisMinute': suggestionsThisMinute,
|
||||
'maxVotesPerHour': maxVotesPerHour,
|
||||
'maxVotesPerMinute': maxVotesPerMinute,
|
||||
'maxSuggestionsPerHour': maxSuggestionsPerHour,
|
||||
'maxSuggestionsPerMinute': maxSuggestionsPerMinute,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user is blocked
|
||||
bool _isUserBlocked(String userId) {
|
||||
final blockTime = _blockedUsers[userId];
|
||||
if (blockTime == null) return false;
|
||||
|
||||
// Unblock after 5 minutes
|
||||
if (DateTime.now().difference(blockTime).inMinutes >= 5) {
|
||||
_blockedUsers.remove(userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
bool _isOnCooldown(String userId, Map<String, DateTime> lastActionTime, int cooldownSeconds) {
|
||||
final lastAction = lastActionTime[userId];
|
||||
if (lastAction == null) return false;
|
||||
|
||||
return DateTime.now().difference(lastAction).inSeconds < cooldownSeconds;
|
||||
}
|
||||
|
||||
// Check rate limits
|
||||
bool _checkRateLimit(String userId, Map<String, List<DateTime>> userActions, int maxPerMinute, int maxPerHour) {
|
||||
final actions = userActions[userId] ?? [];
|
||||
final now = DateTime.now();
|
||||
|
||||
// Count actions in the last minute
|
||||
final actionsLastMinute = actions.where((time) =>
|
||||
now.difference(time).inMinutes < 1).length;
|
||||
if (actionsLastMinute >= maxPerMinute) return false;
|
||||
|
||||
// Count actions in the last hour
|
||||
final actionsLastHour = actions.where((time) =>
|
||||
now.difference(time).inHours < 1).length;
|
||||
if (actionsLastHour >= maxPerHour) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clean old entries (older than 1 hour)
|
||||
void _cleanOldEntries(List<DateTime> entries) {
|
||||
final oneHourAgo = DateTime.now().subtract(const Duration(hours: 1));
|
||||
entries.removeWhere((time) => time.isBefore(oneHourAgo));
|
||||
}
|
||||
|
||||
// Track consecutive actions for spam detection
|
||||
void _incrementConsecutiveActions(String userId) {
|
||||
_consecutiveActions[userId] = (_consecutiveActions[userId] ?? 0) + 1;
|
||||
|
||||
// Reset after some time of inactivity
|
||||
Timer(const Duration(seconds: 30), () {
|
||||
_consecutiveActions[userId] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Check for spam behavior and block if necessary
|
||||
void _checkForSpam(String userId) {
|
||||
final consecutive = _consecutiveActions[userId] ?? 0;
|
||||
|
||||
// Block user if too many consecutive actions
|
||||
if (consecutive > 15) {
|
||||
_blockedUsers[userId] = DateTime.now();
|
||||
if (kDebugMode) {
|
||||
print('User $userId temporarily blocked for spam behavior');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get block status message
|
||||
String? getBlockMessage(String userId) {
|
||||
final blockTime = _blockedUsers[userId];
|
||||
if (blockTime == null) return null;
|
||||
|
||||
final remaining = 5 - DateTime.now().difference(blockTime).inMinutes;
|
||||
if (remaining <= 0) {
|
||||
_blockedUsers.remove(userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'You are temporarily blocked for $remaining more minutes due to excessive activity.';
|
||||
}
|
||||
|
||||
// Clear all data (for testing)
|
||||
void clearAllData() {
|
||||
_userVotes.clear();
|
||||
_userSuggestions.clear();
|
||||
_lastVoteTime.clear();
|
||||
_lastSuggestionTime.clear();
|
||||
_consecutiveActions.clear();
|
||||
_blockedUsers.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
125
CHALLENGE_2/sleepysound/lib/widgets/user_activity_status.dart
Normal file
125
CHALLENGE_2/sleepysound/lib/widgets/user_activity_status.dart
Normal file
|
@ -0,0 +1,125 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/spam_protection_service.dart';
|
||||
|
||||
class UserActivityStatus extends StatelessWidget {
|
||||
final SpamProtectionService spamService;
|
||||
|
||||
const UserActivityStatus({
|
||||
super.key,
|
||||
required this.spamService,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userId = spamService.getCurrentUserId();
|
||||
final stats = spamService.getUserStats(userId);
|
||||
final blockMessage = spamService.getBlockMessage(userId);
|
||||
|
||||
if (blockMessage != null) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.block, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
blockMessage,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Show activity limits if user is getting close
|
||||
final votesUsed = stats['votesThisHour']!;
|
||||
final suggestionsUsed = stats['suggestionsThisHour']!;
|
||||
final maxVotes = stats['maxVotesPerHour']!;
|
||||
final maxSuggestions = stats['maxSuggestionsPerHour']!;
|
||||
|
||||
final showWarning = votesUsed > maxVotes * 0.8 || suggestionsUsed > maxSuggestions * 0.8;
|
||||
|
||||
if (!showWarning) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Activity Limits',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Votes: $votesUsed/$maxVotes',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: votesUsed / maxVotes,
|
||||
backgroundColor: Colors.grey.withOpacity(0.3),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
votesUsed > maxVotes * 0.9 ? Colors.red : Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Suggestions: $suggestionsUsed/$maxSuggestions',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: suggestionsUsed / maxSuggestions,
|
||||
backgroundColor: Colors.grey.withOpacity(0.3),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
suggestionsUsed > maxSuggestions * 0.9 ? Colors.red : Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue