icon things
|
@ -8,7 +8,7 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.sleepysound"
|
namespace = "com.example.sleepysound"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 206 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#121212</color>
|
||||||
|
</resources>
|
BIN
CHALLENGE_2/sleepysound/assets/icons/app_icon.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
CHALLENGE_2/sleepysound/assets/icons/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
|
@ -427,7 +427,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
@ -484,7 +484,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 780 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 25 KiB |
|
@ -26,7 +26,7 @@ class MyApp extends StatelessWidget {
|
||||||
ChangeNotifierProvider(create: (context) => AudioService()),
|
ChangeNotifierProvider(create: (context) => AudioService()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'SleepySound',
|
title: 'LidoSound',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/network_group_service.dart';
|
import '../services/network_group_service.dart';
|
||||||
import '../widgets/network_demo_widget.dart';
|
|
||||||
|
|
||||||
class GroupPage extends StatefulWidget {
|
class GroupPage extends StatefulWidget {
|
||||||
const GroupPage({super.key});
|
const GroupPage({super.key});
|
||||||
|
@ -172,9 +171,6 @@ class _GroupPageState extends State<GroupPage> {
|
||||||
|
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
// Demo Widget
|
|
||||||
NetworkDemoWidget(networkService: networkService),
|
|
||||||
|
|
||||||
// Network Users Section
|
// Network Users Section
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
@ -330,13 +326,34 @@ class _GroupPageState extends State<GroupPage> {
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
if (user.isListening && user.currentTrackName != null) ...[
|
||||||
'Joined ${_formatDuration(DateTime.now().difference(user.joinedAt))} ago',
|
const Icon(
|
||||||
style: const TextStyle(
|
Icons.music_note,
|
||||||
color: Colors.grey,
|
color: Color(0xFF6366F1),
|
||||||
fontSize: 12,
|
size: 14,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Listening to "${user.currentTrackName}" by ${user.currentArtist}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF6366F1),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
'Joined ${_formatDuration(DateTime.now().difference(user.joinedAt))} ago',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
if (!user.isOnline) ...[
|
if (!user.isOnline) ...[
|
||||||
const Text(
|
const Text(
|
||||||
' • ',
|
' • ',
|
||||||
|
@ -366,20 +383,54 @@ class _GroupPageState extends State<GroupPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
children: [
|
||||||
decoration: BoxDecoration(
|
if (user.isListening && !isCurrentUser) ...[
|
||||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
// Join Listening Session Button
|
||||||
borderRadius: BorderRadius.circular(10),
|
IconButton(
|
||||||
),
|
onPressed: () async {
|
||||||
child: Text(
|
final success = await networkService.joinListeningSession(user);
|
||||||
'${user.votes} votes',
|
if (mounted) {
|
||||||
style: const TextStyle(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
color: Color(0xFF6366F1),
|
SnackBar(
|
||||||
fontSize: 11,
|
content: Text(
|
||||||
fontWeight: FontWeight.bold,
|
success
|
||||||
|
? 'Joined ${user.name}\'s listening session! 🎵'
|
||||||
|
: 'Failed to join listening session',
|
||||||
|
),
|
||||||
|
backgroundColor: success
|
||||||
|
? const Color(0xFF22C55E)
|
||||||
|
: const Color(0xFFEF4444),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.headphones,
|
||||||
|
color: Color(0xFF6366F1),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
tooltip: 'Join listening session',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${user.votes} votes',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF6366F1),
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,14 +15,188 @@ class VotingPage extends StatefulWidget {
|
||||||
|
|
||||||
class _VotingPageState extends State<VotingPage> {
|
class _VotingPageState extends State<VotingPage> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final FocusNode _searchFocusNode = FocusNode();
|
||||||
List<SpotifyTrack> _searchResults = [];
|
List<SpotifyTrack> _searchResults = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String _statusMessage = '';
|
String _statusMessage = '';
|
||||||
|
|
||||||
|
final LayerLink _layerLink = LayerLink();
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadInitialQueue();
|
_loadInitialQueue();
|
||||||
|
_searchFocusNode.addListener(_onSearchFocusChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hideSearchOverlay();
|
||||||
|
_searchController.dispose();
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearchFocusChange() {
|
||||||
|
if (_searchFocusNode.hasFocus && _searchResults.isNotEmpty) {
|
||||||
|
_showSearchOverlay();
|
||||||
|
} else if (!_searchFocusNode.hasFocus) {
|
||||||
|
// Delay hiding to allow for taps on results
|
||||||
|
Future.delayed(const Duration(milliseconds: 150), () {
|
||||||
|
_hideSearchOverlay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSearchOverlay() {
|
||||||
|
if (_overlayEntry != null) return;
|
||||||
|
|
||||||
|
_overlayEntry = _createOverlayEntry();
|
||||||
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideSearchOverlay() {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayEntry _createOverlayEntry() {
|
||||||
|
RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
var size = renderBox.size;
|
||||||
|
var offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
|
||||||
|
return OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
left: offset.dx + 20,
|
||||||
|
top: offset.dy + 200, // Adjust based on search field position
|
||||||
|
width: size.width - 40,
|
||||||
|
child: CompositedTransformFollower(
|
||||||
|
link: _layerLink,
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
child: Material(
|
||||||
|
elevation: 8,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 300),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFF6366F1).withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: _buildSearchResultsOverlay(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchResultsOverlay() {
|
||||||
|
if (_searchResults.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: const Text(
|
||||||
|
'No results found',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: _searchResults.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final track = _searchResults[index];
|
||||||
|
return _buildSearchResultItem(track, index);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchResultItem(SpotifyTrack track, int index) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_addToQueue(track);
|
||||||
|
_hideSearchOverlay();
|
||||||
|
_searchController.clear();
|
||||||
|
_searchFocusNode.unfocus();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Album Art
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
color: const Color(0xFF2A2A2A),
|
||||||
|
),
|
||||||
|
child: track.album.images.isNotEmpty
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: Image.network(
|
||||||
|
track.album.images.first.url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Icon(
|
||||||
|
Icons.music_note,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 16,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.music_note,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Track Info
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
track.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
track.artists.map((a) => a.name).join(', '),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Add Icon
|
||||||
|
const Icon(
|
||||||
|
Icons.add_circle_outline,
|
||||||
|
color: Color(0xFF6366F1),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadInitialQueue() async {
|
Future<void> _loadInitialQueue() async {
|
||||||
|
@ -31,7 +205,14 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _searchSpotify(String query) async {
|
Future<void> _searchSpotify(String query) async {
|
||||||
if (query.isEmpty) return;
|
if (query.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_statusMessage = '';
|
||||||
|
});
|
||||||
|
_hideSearchOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if search query is appropriate
|
// Check if search query is appropriate
|
||||||
if (!GenreFilterService.isSearchQueryAppropriate(query)) {
|
if (!GenreFilterService.isSearchQueryAppropriate(query)) {
|
||||||
|
@ -41,6 +222,7 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
_statusMessage = 'Search term not suitable for the peaceful Lido atmosphere. Try: ${suggestions.join(', ')}';
|
_statusMessage = 'Search term not suitable for the peaceful Lido atmosphere. Try: ${suggestions.join(', ')}';
|
||||||
_searchResults = [];
|
_searchResults = [];
|
||||||
});
|
});
|
||||||
|
_hideSearchOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +239,7 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
_statusMessage = blockMessage ?? 'Please wait $cooldown seconds before searching again.';
|
_statusMessage = blockMessage ?? 'Please wait $cooldown seconds before searching again.';
|
||||||
_searchResults = [];
|
_searchResults = [];
|
||||||
});
|
});
|
||||||
|
_hideSearchOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,31 +252,35 @@ 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
|
// No filtering on search results - let users see all tracks
|
||||||
final filteredResults = GenreFilterService.filterSearchResults(results);
|
// Filtering only happens when adding to queue to maintain atmosphere
|
||||||
|
|
||||||
// Record the suggestion attempt
|
// Record the suggestion attempt
|
||||||
spamService.recordSuggestion(userId);
|
spamService.recordSuggestion(userId);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_searchResults = filteredResults;
|
_searchResults = results;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
if (filteredResults.isEmpty && results.isNotEmpty) {
|
if (results.isEmpty) {
|
||||||
_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"';
|
_statusMessage = 'No tracks found for "$query"';
|
||||||
} else {
|
} else {
|
||||||
final filtered = results.length - filteredResults.length;
|
_statusMessage = 'Found ${results.length} tracks';
|
||||||
_statusMessage = 'Found ${filteredResults.length} tracks' +
|
|
||||||
(filtered > 0 ? ' ($filtered filtered for atmosphere)' : '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show overlay if we have results and search field is focused
|
||||||
|
if (results.isNotEmpty && _searchFocusNode.hasFocus) {
|
||||||
|
_showSearchOverlay();
|
||||||
|
} else {
|
||||||
|
_hideSearchOverlay();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_statusMessage = 'Search failed: ${e.toString()}';
|
_statusMessage = 'Search failed: ${e.toString()}';
|
||||||
_searchResults = [];
|
_searchResults = [];
|
||||||
});
|
});
|
||||||
|
_hideSearchOverlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,34 +389,74 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Search Bar
|
// Search Bar
|
||||||
TextField(
|
CompositedTransformTarget(
|
||||||
controller: _searchController,
|
link: _layerLink,
|
||||||
style: const TextStyle(color: Colors.white),
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
controller: _searchController,
|
||||||
hintText: 'Search for songs, artists, albums...',
|
focusNode: _searchFocusNode,
|
||||||
hintStyle: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.white),
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
decoration: InputDecoration(
|
||||||
suffixIcon: _isLoading
|
hintText: 'Search for songs, artists, albums...',
|
||||||
? const Padding(
|
hintStyle: const TextStyle(color: Colors.grey),
|
||||||
padding: EdgeInsets.all(12),
|
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||||
child: SizedBox(
|
suffixIcon: Row(
|
||||||
width: 20,
|
mainAxisSize: MainAxisSize.min,
|
||||||
height: 20,
|
children: [
|
||||||
child: CircularProgressIndicator(
|
if (_isLoading)
|
||||||
strokeWidth: 2,
|
const Padding(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6366F1)),
|
padding: EdgeInsets.all(12),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6366F1)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
if (_searchController.text.isNotEmpty)
|
||||||
: null,
|
IconButton(
|
||||||
filled: true,
|
icon: const Icon(Icons.clear, color: Colors.grey),
|
||||||
fillColor: const Color(0xFF1E1E1E),
|
onPressed: () {
|
||||||
border: OutlineInputBorder(
|
_searchController.clear();
|
||||||
borderRadius: BorderRadius.circular(12),
|
_hideSearchOverlay();
|
||||||
borderSide: BorderSide.none,
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_statusMessage = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFF1E1E1E),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF6366F1), width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
// Search as user types (with debounce)
|
||||||
|
if (value.length >= 3) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (_searchController.text == value) {
|
||||||
|
_searchSpotify(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (value.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_statusMessage = '';
|
||||||
|
});
|
||||||
|
_hideSearchOverlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitted: _searchSpotify,
|
||||||
),
|
),
|
||||||
onSubmitted: _searchSpotify,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Atmosphere Info
|
// Atmosphere Info
|
||||||
|
@ -323,33 +550,51 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Search Results and Queue
|
// Queue Section
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DefaultTabController(
|
child: Column(
|
||||||
length: 2,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
const TabBar(
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
labelColor: Color(0xFF6366F1),
|
child: Row(
|
||||||
unselectedLabelColor: Colors.grey,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
indicatorColor: Color(0xFF6366F1),
|
children: [
|
||||||
tabs: [
|
const Text(
|
||||||
Tab(text: 'Search Results'),
|
'Music Queue',
|
||||||
Tab(text: 'Queue'),
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFF6366F1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${queueService.queue.length} songs',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF6366F1),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TabBarView(
|
const SizedBox(height: 16),
|
||||||
children: [
|
Expanded(
|
||||||
// Search Results Tab
|
child: _buildQueueView(queueService),
|
||||||
_buildSearchResults(),
|
),
|
||||||
// Queue Tab
|
],
|
||||||
_buildQueueView(queueService),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -360,40 +605,6 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchResults() {
|
|
||||||
if (_searchResults.isEmpty && !_isLoading) {
|
|
||||||
return const Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.search,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'Search for songs to add to the queue',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
itemCount: _searchResults.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final track = _searchResults[index];
|
|
||||||
return _buildTrackCard(track);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQueueView(MusicQueueService queueService) {
|
Widget _buildQueueView(MusicQueueService queueService) {
|
||||||
final queue = queueService.queue;
|
final queue = queueService.queue;
|
||||||
|
|
||||||
|
@ -439,97 +650,6 @@ class _VotingPageState extends State<VotingPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTrackCard(SpotifyTrack track) {
|
|
||||||
return Card(
|
|
||||||
color: const Color(0xFF1E1E1E),
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// Album Art
|
|
||||||
Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: const Color(0xFF2A2A2A),
|
|
||||||
),
|
|
||||||
child: track.album.images.isNotEmpty
|
|
||||||
? ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Image.network(
|
|
||||||
track.album.images.first.url,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return const Icon(
|
|
||||||
Icons.music_note,
|
|
||||||
color: Colors.grey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(
|
|
||||||
Icons.music_note,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
|
|
||||||
// Track Info
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
track.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
track.artists.map((a) => a.name).join(', '),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
track.album.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add Button
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => _addToQueue(track),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.add_circle,
|
|
||||||
color: Color(0xFF6366F1),
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQueueItemCard(QueueItem queueItem, int index, MusicQueueService queueService) {
|
Widget _buildQueueItemCard(QueueItem queueItem, int index, MusicQueueService queueService) {
|
||||||
return Card(
|
return Card(
|
||||||
color: const Color(0xFF1E1E1E),
|
color: const Color(0xFF1E1E1E),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:network_info_plus/network_info_plus.dart';
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
import 'package:multicast_dns/multicast_dns.dart';
|
import 'package:multicast_dns/multicast_dns.dart';
|
||||||
|
import 'music_queue_service.dart';
|
||||||
|
|
||||||
class NetworkUser {
|
class NetworkUser {
|
||||||
final String id;
|
final String id;
|
||||||
|
@ -15,6 +16,11 @@ class NetworkUser {
|
||||||
final int votes;
|
final int votes;
|
||||||
bool isOnline;
|
bool isOnline;
|
||||||
DateTime lastSeen;
|
DateTime lastSeen;
|
||||||
|
String? currentTrackId;
|
||||||
|
String? currentTrackName;
|
||||||
|
String? currentArtist;
|
||||||
|
String? currentTrackImage;
|
||||||
|
bool isListening;
|
||||||
|
|
||||||
NetworkUser({
|
NetworkUser({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -24,6 +30,11 @@ class NetworkUser {
|
||||||
this.votes = 0,
|
this.votes = 0,
|
||||||
this.isOnline = true,
|
this.isOnline = true,
|
||||||
DateTime? lastSeen,
|
DateTime? lastSeen,
|
||||||
|
this.currentTrackId,
|
||||||
|
this.currentTrackName,
|
||||||
|
this.currentArtist,
|
||||||
|
this.currentTrackImage,
|
||||||
|
this.isListening = false,
|
||||||
}) : lastSeen = lastSeen ?? DateTime.now();
|
}) : lastSeen = lastSeen ?? DateTime.now();
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
@ -34,6 +45,11 @@ class NetworkUser {
|
||||||
'votes': votes,
|
'votes': votes,
|
||||||
'isOnline': isOnline,
|
'isOnline': isOnline,
|
||||||
'lastSeen': lastSeen.toIso8601String(),
|
'lastSeen': lastSeen.toIso8601String(),
|
||||||
|
'currentTrackId': currentTrackId,
|
||||||
|
'currentTrackName': currentTrackName,
|
||||||
|
'currentArtist': currentArtist,
|
||||||
|
'currentTrackImage': currentTrackImage,
|
||||||
|
'isListening': isListening,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory NetworkUser.fromJson(Map<String, dynamic> json) => NetworkUser(
|
factory NetworkUser.fromJson(Map<String, dynamic> json) => NetworkUser(
|
||||||
|
@ -44,6 +60,11 @@ class NetworkUser {
|
||||||
votes: json['votes'] ?? 0,
|
votes: json['votes'] ?? 0,
|
||||||
isOnline: json['isOnline'] ?? true,
|
isOnline: json['isOnline'] ?? true,
|
||||||
lastSeen: DateTime.parse(json['lastSeen']),
|
lastSeen: DateTime.parse(json['lastSeen']),
|
||||||
|
currentTrackId: json['currentTrackId'],
|
||||||
|
currentTrackName: json['currentTrackName'],
|
||||||
|
currentArtist: json['currentArtist'],
|
||||||
|
currentTrackImage: json['currentTrackImage'],
|
||||||
|
isListening: json['isListening'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +87,7 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
|
|
||||||
final Map<String, NetworkUser> _networkUsers = {};
|
final Map<String, NetworkUser> _networkUsers = {};
|
||||||
late NetworkUser _currentUser;
|
late NetworkUser _currentUser;
|
||||||
|
MusicQueueService? _musicService;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool get isConnectedToWifi => _isConnectedToWifi;
|
bool get isConnectedToWifi => _isConnectedToWifi;
|
||||||
|
@ -79,6 +101,8 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
NetworkGroupService() {
|
NetworkGroupService() {
|
||||||
_initializeCurrentUser();
|
_initializeCurrentUser();
|
||||||
_startNetworkMonitoring();
|
_startNetworkMonitoring();
|
||||||
|
// Initialize music service reference
|
||||||
|
_musicService = MusicQueueService();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeCurrentUser() {
|
void _initializeCurrentUser() {
|
||||||
|
@ -232,6 +256,8 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
if (request.method == 'GET' && request.uri.path == '/user') {
|
if (request.method == 'GET' && request.uri.path == '/user') {
|
||||||
|
// Update current user with latest listening info before sending
|
||||||
|
await _updateCurrentUserListeningInfo();
|
||||||
// Return current user info
|
// Return current user info
|
||||||
response.write(jsonEncode(_currentUser.toJson()));
|
response.write(jsonEncode(_currentUser.toJson()));
|
||||||
} else if (request.method == 'POST' && request.uri.path == '/heartbeat') {
|
} else if (request.method == 'POST' && request.uri.path == '/heartbeat') {
|
||||||
|
@ -244,6 +270,41 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
response.write(jsonEncode({'status': 'ok'}));
|
response.write(jsonEncode({'status': 'ok'}));
|
||||||
|
} else if (request.method == 'POST' && request.uri.path == '/join-session') {
|
||||||
|
// Handle request to join this user's listening session
|
||||||
|
final body = await utf8.decoder.bind(request).join();
|
||||||
|
// Parse request data for future use (logging, analytics, etc.)
|
||||||
|
jsonDecode(body);
|
||||||
|
|
||||||
|
// Get current track info to send back
|
||||||
|
final currentTrackInfo = _musicService?.currentTrackInfo;
|
||||||
|
if (currentTrackInfo != null) {
|
||||||
|
response.write(jsonEncode({
|
||||||
|
'status': 'ok',
|
||||||
|
'trackInfo': currentTrackInfo,
|
||||||
|
'message': 'Successfully joined listening session'
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
response.write(jsonEncode({
|
||||||
|
'status': 'no_track',
|
||||||
|
'message': 'No track currently playing'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (request.method == 'GET' && request.uri.path == '/current-track') {
|
||||||
|
// Get current track info without joining
|
||||||
|
await _updateCurrentUserListeningInfo();
|
||||||
|
final currentTrackInfo = _musicService?.currentTrackInfo;
|
||||||
|
if (currentTrackInfo != null) {
|
||||||
|
response.write(jsonEncode({
|
||||||
|
'status': 'ok',
|
||||||
|
'trackInfo': currentTrackInfo
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
response.write(jsonEncode({
|
||||||
|
'status': 'no_track',
|
||||||
|
'message': 'No track currently playing'
|
||||||
|
}));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
response.statusCode = 404;
|
response.statusCode = 404;
|
||||||
response.write(jsonEncode({'error': 'Not found'}));
|
response.write(jsonEncode({'error': 'Not found'}));
|
||||||
|
@ -395,6 +456,39 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateCurrentUserListeningInfo() async {
|
||||||
|
final currentTrackInfo = _musicService?.currentTrackInfo;
|
||||||
|
final currentTrack = _musicService?.currentTrack;
|
||||||
|
|
||||||
|
if (currentTrack != null && currentTrackInfo != null) {
|
||||||
|
_currentUser = NetworkUser(
|
||||||
|
id: _currentUser.id,
|
||||||
|
name: _currentUser.name,
|
||||||
|
ipAddress: _currentUser.ipAddress,
|
||||||
|
joinedAt: _currentUser.joinedAt,
|
||||||
|
votes: _currentUser.votes,
|
||||||
|
isOnline: true,
|
||||||
|
currentTrackId: currentTrack.id,
|
||||||
|
currentTrackName: currentTrack.name,
|
||||||
|
currentArtist: currentTrack.artistNames,
|
||||||
|
currentTrackImage: currentTrack.imageUrl,
|
||||||
|
isListening: currentTrackInfo['isPlaying'] ?? false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_currentUser = NetworkUser(
|
||||||
|
id: _currentUser.id,
|
||||||
|
name: _currentUser.name,
|
||||||
|
ipAddress: _currentUser.ipAddress,
|
||||||
|
joinedAt: _currentUser.joinedAt,
|
||||||
|
votes: _currentUser.votes,
|
||||||
|
isOnline: true,
|
||||||
|
isListening: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_networkUsers[_currentUser.id] = _currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
// Public methods for UI interaction
|
// Public methods for UI interaction
|
||||||
Future<void> refreshNetwork() async {
|
Future<void> refreshNetwork() async {
|
||||||
await _checkConnectivity();
|
await _checkConnectivity();
|
||||||
|
@ -426,6 +520,45 @@ class NetworkGroupService extends ChangeNotifier {
|
||||||
return 'Connected to $_currentNetworkName';
|
return 'Connected to $_currentNetworkName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Join another user's listening session
|
||||||
|
Future<bool> joinListeningSession(NetworkUser user) async {
|
||||||
|
if (!user.isListening || user.currentTrackId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final client = HttpClient();
|
||||||
|
client.connectionTimeout = const Duration(seconds: 5);
|
||||||
|
|
||||||
|
final request = await client.postUrl(
|
||||||
|
Uri.parse('http://${user.ipAddress}:$_discoveryPort/join-session')
|
||||||
|
);
|
||||||
|
request.headers.set('Content-Type', 'application/json');
|
||||||
|
request.write(jsonEncode({'userId': _currentUser.id}));
|
||||||
|
|
||||||
|
final response = await request.close().timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final body = await utf8.decoder.bind(response).join();
|
||||||
|
final responseData = jsonDecode(body);
|
||||||
|
|
||||||
|
if (responseData['status'] == 'ok' && responseData['trackInfo'] != null) {
|
||||||
|
// Here you could sync the track with your local player
|
||||||
|
// For now, we'll just return success
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Error joining listening session: $e');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Demo methods for testing
|
// Demo methods for testing
|
||||||
void simulateNetworkConnection() {
|
void simulateNetworkConnection() {
|
||||||
_isConnectedToWifi = true;
|
_isConnectedToWifi = true;
|
||||||
|
|
|
@ -34,7 +34,13 @@ class SpamProtectionService extends ChangeNotifier {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cooldown
|
// Allow first vote without cooldown for smooth user experience
|
||||||
|
final votes = _userVotes[userId] ?? [];
|
||||||
|
if (votes.isEmpty) {
|
||||||
|
return true; // First vote is always allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown for subsequent votes
|
||||||
if (_isOnCooldown(userId, _lastVoteTime, voteCooldown)) {
|
if (_isOnCooldown(userId, _lastVoteTime, voteCooldown)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +60,13 @@ class SpamProtectionService extends ChangeNotifier {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cooldown
|
// Allow first suggestion without cooldown for smooth user experience
|
||||||
|
final suggestions = _userSuggestions[userId] ?? [];
|
||||||
|
if (suggestions.isEmpty) {
|
||||||
|
return true; // First suggestion is always allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown for subsequent suggestions
|
||||||
if (_isOnCooldown(userId, _lastSuggestionTime, suggestionCooldown)) {
|
if (_isOnCooldown(userId, _lastSuggestionTime, suggestionCooldown)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.7.1"
|
version: "7.7.1"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -177,6 +185,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -294,6 +310,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -360,6 +384,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.4"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -600,6 +632,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.1"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -77,6 +77,9 @@ dev_dependencies:
|
||||||
# JSON serialization
|
# JSON serialization
|
||||||
json_serializable: ^6.7.1
|
json_serializable: ^6.7.1
|
||||||
build_runner: ^2.4.7
|
build_runner: ^2.4.7
|
||||||
|
|
||||||
|
# App icon generator
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
@ -92,6 +95,7 @@ flutter:
|
||||||
# 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/
|
- assets/audio/
|
||||||
|
- assets/icons/
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
@ -120,3 +124,13 @@ flutter:
|
||||||
#
|
#
|
||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/to/font-from-package
|
# see https://flutter.dev/to/font-from-package
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
web:
|
||||||
|
generate: true
|
||||||
|
image_path: "assets/icons/app_icon.png"
|
||||||
|
adaptive_icon_background: "#121212"
|
||||||
|
adaptive_icon_foreground: "assets/icons/app_icon_foreground.png"
|
||||||
|
remove_alpha_ios: true
|
||||||
|
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 581 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 312 KiB |
|
@ -32,4 +32,4 @@
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|