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 'pages/group_page.dart';
|
||||||
import 'services/music_queue_service.dart';
|
import 'services/music_queue_service.dart';
|
||||||
import 'services/network_group_service.dart';
|
import 'services/network_group_service.dart';
|
||||||
|
import 'services/spam_protection_service.dart';
|
||||||
|
import 'services/audio_service.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
@ -20,6 +22,8 @@ class MyApp extends StatelessWidget {
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => MusicQueueService()),
|
ChangeNotifierProvider(create: (context) => MusicQueueService()),
|
||||||
ChangeNotifierProvider(create: (context) => NetworkGroupService()),
|
ChangeNotifierProvider(create: (context) => NetworkGroupService()),
|
||||||
|
ChangeNotifierProvider(create: (context) => SpamProtectionService()),
|
||||||
|
ChangeNotifierProvider(create: (context) => AudioService()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'SleepySound',
|
title: 'SleepySound',
|
||||||
|
@ -81,10 +85,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
title: Text(_getPageTitle()),
|
|
||||||
),
|
|
||||||
body: _getSelectedPage(),
|
body: _getSelectedPage(),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
items: const <BottomNavigationBarItem>[
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/music_queue_service.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 '../models/spotify_track.dart';
|
||||||
|
import '../widgets/user_activity_status.dart';
|
||||||
|
|
||||||
class VotingPage extends StatefulWidget {
|
class VotingPage extends StatefulWidget {
|
||||||
const VotingPage({super.key});
|
const VotingPage({super.key});
|
||||||
|
@ -30,6 +33,33 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
Future<void> _searchSpotify(String query) async {
|
Future<void> _searchSpotify(String query) async {
|
||||||
if (query.isEmpty) return;
|
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(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_statusMessage = 'Searching for "$query"...';
|
_statusMessage = 'Searching for "$query"...';
|
||||||
|
@ -39,12 +69,24 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
final queueService = Provider.of<MusicQueueService>(context, listen: false);
|
final queueService = Provider.of<MusicQueueService>(context, listen: false);
|
||||||
final results = await queueService.searchTracks(query);
|
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(() {
|
setState(() {
|
||||||
_searchResults = results;
|
_searchResults = filteredResults;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_statusMessage = results.isEmpty
|
if (filteredResults.isEmpty && results.isNotEmpty) {
|
||||||
? 'No tracks found for "$query"'
|
_statusMessage = 'No tracks found that match the peaceful Lido atmosphere. Try searching for chill, ambient, or relaxing music.';
|
||||||
: 'Found ${results.length} tracks';
|
} 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) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -56,9 +98,43 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addToQueue(SpotifyTrack track) {
|
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);
|
final queueService = Provider.of<MusicQueueService>(context, listen: false);
|
||||||
queueService.addToQueue(track);
|
queueService.addToQueue(track);
|
||||||
|
|
||||||
|
// Record the suggestion
|
||||||
|
spamService.recordSuggestion(userId);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Added "${track.name}" to queue'),
|
content: Text('Added "${track.name}" to queue'),
|
||||||
|
@ -77,6 +153,13 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
// User Activity Status
|
||||||
|
Consumer<SpamProtectionService>(
|
||||||
|
builder: (context, spamService, child) {
|
||||||
|
return UserActivityStatus(spamService: spamService);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Header with Search
|
// Header with Search
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
@ -149,6 +232,81 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
onSubmitted: _searchSpotify,
|
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
|
// Status Message
|
||||||
if (_statusMessage.isNotEmpty)
|
if (_statusMessage.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -458,13 +616,25 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
// Voting Buttons
|
// Voting Buttons
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
Consumer<SpamProtectionService>(
|
||||||
onPressed: () => queueService.upvote(index),
|
builder: (context, spamService, child) {
|
||||||
icon: const Icon(
|
final userId = spamService.getCurrentUserId();
|
||||||
Icons.keyboard_arrow_up,
|
final canVote = spamService.canVote(userId);
|
||||||
color: Colors.green,
|
final cooldown = spamService.getVoteCooldownRemaining(userId);
|
||||||
size: 28,
|
|
||||||
),
|
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(
|
Text(
|
||||||
'${queueItem.votes}',
|
'${queueItem.votes}',
|
||||||
|
@ -473,13 +643,25 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
Consumer<SpamProtectionService>(
|
||||||
onPressed: () => queueService.downvote(index),
|
builder: (context, spamService, child) {
|
||||||
icon: const Icon(
|
final userId = spamService.getCurrentUserId();
|
||||||
Icons.keyboard_arrow_down,
|
final canVote = spamService.canVote(userId);
|
||||||
color: Colors.red,
|
final cooldown = spamService.getVoteCooldownRemaining(userId);
|
||||||
size: 28,
|
|
||||||
),
|
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 'package:flutter/foundation.dart';
|
||||||
import '../models/spotify_track.dart';
|
import '../models/spotify_track.dart';
|
||||||
import '../services/spotify_service.dart';
|
import '../services/spotify_service.dart';
|
||||||
|
import '../services/audio_service.dart';
|
||||||
|
|
||||||
class QueueItem {
|
class QueueItem {
|
||||||
final SpotifyTrack track;
|
final SpotifyTrack track;
|
||||||
|
@ -33,11 +34,10 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
MusicQueueService._internal();
|
MusicQueueService._internal();
|
||||||
|
|
||||||
final SpotifyService _spotifyService = SpotifyService();
|
final SpotifyService _spotifyService = SpotifyService();
|
||||||
|
final AudioService _audioService = AudioService();
|
||||||
|
|
||||||
// Current playing track
|
// Current playing track
|
||||||
SpotifyTrack? _currentTrack;
|
SpotifyTrack? _currentTrack;
|
||||||
bool _isPlaying = false;
|
|
||||||
double _progress = 0.0;
|
|
||||||
|
|
||||||
// Queue management
|
// Queue management
|
||||||
final List<QueueItem> _queue = [];
|
final List<QueueItem> _queue = [];
|
||||||
|
@ -46,9 +46,9 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
final List<SpotifyTrack> _recentlyPlayed = [];
|
final List<SpotifyTrack> _recentlyPlayed = [];
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
SpotifyTrack? get currentTrack => _currentTrack;
|
SpotifyTrack? get currentTrack => _audioService.currentTrack ?? _currentTrack;
|
||||||
bool get isPlaying => _isPlaying;
|
bool get isPlaying => _audioService.isPlaying;
|
||||||
double get progress => _progress;
|
double get progress => _audioService.progress;
|
||||||
List<QueueItem> get queue => List.unmodifiable(_queue);
|
List<QueueItem> get queue => List.unmodifiable(_queue);
|
||||||
List<SpotifyTrack> get recentlyPlayed => List.unmodifiable(_recentlyPlayed);
|
List<SpotifyTrack> get recentlyPlayed => List.unmodifiable(_recentlyPlayed);
|
||||||
|
|
||||||
|
@ -102,13 +102,14 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playback simulation
|
// Playback control
|
||||||
void playNext() {
|
Future<void> playNext() async {
|
||||||
if (_queue.isNotEmpty) {
|
if (_queue.isNotEmpty) {
|
||||||
final nextItem = _queue.removeAt(0);
|
final nextItem = _queue.removeAt(0);
|
||||||
_currentTrack = nextItem.track;
|
_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
|
// Add to recently played
|
||||||
_recentlyPlayed.insert(0, nextItem.track);
|
_recentlyPlayed.insert(0, nextItem.track);
|
||||||
|
@ -118,37 +119,25 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
print('Now playing: ${_currentTrack!.name} by ${_currentTrack!.artistNames}');
|
print('Now playing: ${_currentTrack!.name} by ${_currentTrack!.artistNames}');
|
||||||
|
|
||||||
// Simulate track progress
|
|
||||||
_simulatePlayback();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void togglePlayPause() {
|
Future<void> togglePlayPause() async {
|
||||||
_isPlaying = !_isPlaying;
|
await _audioService.togglePlayPause();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipTrack() {
|
Future<void> skipTrack() async {
|
||||||
playNext();
|
await playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _simulatePlayback() {
|
Future<void> seekTo(double position) async {
|
||||||
if (_currentTrack == null) return;
|
if (_audioService.totalDuration != Duration.zero) {
|
||||||
|
final seekPosition = Duration(
|
||||||
// Simulate track progress over time
|
milliseconds: (position * _audioService.totalDuration.inMilliseconds).round(),
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
);
|
||||||
if (_isPlaying && _currentTrack != null) {
|
await _audioService.seekTo(seekPosition);
|
||||||
_progress += 1.0 / (_currentTrack!.durationMs / 1000);
|
}
|
||||||
if (_progress >= 1.0) {
|
|
||||||
// Track finished, play next
|
|
||||||
playNext();
|
|
||||||
} else {
|
|
||||||
notifyListeners();
|
|
||||||
_simulatePlayback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with some popular tracks
|
// Initialize with some popular tracks
|
||||||
|
@ -165,9 +154,14 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
_queue.add(queueItem);
|
_queue.add(queueItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up audio service callback for track completion
|
||||||
|
_audioService.onTrackComplete = () {
|
||||||
|
playNext();
|
||||||
|
};
|
||||||
|
|
||||||
// Start playing the first track
|
// Start playing the first track
|
||||||
if (_queue.isNotEmpty) {
|
if (_queue.isNotEmpty) {
|
||||||
playNext();
|
await playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -189,16 +183,18 @@ class MusicQueueService extends ChangeNotifier {
|
||||||
|
|
||||||
// Get current track info for display
|
// Get current track info for display
|
||||||
Map<String, dynamic>? get currentTrackInfo {
|
Map<String, dynamic>? get currentTrackInfo {
|
||||||
if (_currentTrack == null) return null;
|
final track = currentTrack;
|
||||||
|
if (track == null) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'title': _currentTrack!.name,
|
'title': track.name,
|
||||||
'artist': _currentTrack!.artistNames,
|
'artist': track.artistNames,
|
||||||
'album': _currentTrack!.album.name,
|
'album': track.album.name,
|
||||||
'imageUrl': _currentTrack!.imageUrl,
|
'imageUrl': track.imageUrl,
|
||||||
'duration': _currentTrack!.duration,
|
'duration': _audioService.totalDurationString,
|
||||||
'progress': _progress,
|
'currentTime': _audioService.currentPositionString,
|
||||||
'isPlaying': _isPlaying,
|
'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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,9 +6,13 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
||||||
|
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
audioplayers_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,19 @@
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import audioplayers_darwin
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import network_info_plus
|
import network_info_plus
|
||||||
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
|
|
|
@ -33,6 +33,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.12.0"
|
||||||
|
audioplayers:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audioplayers
|
||||||
|
sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
|
audioplayers_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_android
|
||||||
|
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.1"
|
||||||
|
audioplayers_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_darwin
|
||||||
|
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.0"
|
||||||
|
audioplayers_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_linux
|
||||||
|
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
|
audioplayers_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_platform_interface
|
||||||
|
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.1"
|
||||||
|
audioplayers_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_web
|
||||||
|
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
audioplayers_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_windows
|
||||||
|
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -464,6 +520,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.17"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -645,6 +725,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -677,6 +765,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -773,6 +869,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -59,6 +59,9 @@ dependencies:
|
||||||
|
|
||||||
# Local network discovery
|
# Local network discovery
|
||||||
multicast_dns: ^0.3.2+4
|
multicast_dns: ^0.3.2+4
|
||||||
|
|
||||||
|
# Audio playback
|
||||||
|
audioplayers: ^6.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -87,7 +90,8 @@ flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
|
- assets/audio/
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,13 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AudioplayersWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
audioplayers_windows
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue