spotify and network groups working
This commit is contained in:
parent
025eee7644
commit
6b93f1206d
11 changed files with 1381 additions and 267 deletions
|
@ -1,4 +1,12 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Network permissions for WiFi-based group features -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:label="sleepysound"
|
||||
android:name="${applicationName}"
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'pages/now_playing_page.dart';
|
|||
import 'pages/voting_page.dart';
|
||||
import 'pages/group_page.dart';
|
||||
import 'services/music_queue_service.dart';
|
||||
import 'services/network_group_service.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
@ -15,8 +16,11 @@ class MyApp extends StatelessWidget {
|
|||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => MusicQueueService(),
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => MusicQueueService()),
|
||||
ChangeNotifierProvider(create: (context) => NetworkGroupService()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'SleepySound',
|
||||
theme: ThemeData(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/music_queue_service.dart';
|
||||
import '../services/network_group_service.dart';
|
||||
import '../widgets/network_demo_widget.dart';
|
||||
|
||||
class GroupPage extends StatefulWidget {
|
||||
const GroupPage({super.key});
|
||||
|
@ -10,314 +11,456 @@ class GroupPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _GroupPageState extends State<GroupPage> {
|
||||
bool isConnectedToLido = true; // Simulate location verification
|
||||
String userName = "Guest #${DateTime.now().millisecond}";
|
||||
|
||||
List<Map<String, dynamic>> activeUsers = [
|
||||
{"name": "Alex M.", "joined": "2 min ago", "votes": 5, "isOnline": true},
|
||||
{"name": "Sarah K.", "joined": "5 min ago", "votes": 3, "isOnline": true},
|
||||
{"name": "Marco R.", "joined": "8 min ago", "votes": 7, "isOnline": true},
|
||||
{"name": "Lisa F.", "joined": "12 min ago", "votes": 2, "isOnline": false},
|
||||
{"name": "Tom B.", "joined": "15 min ago", "votes": 4, "isOnline": true},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFF121212),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Location Status Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isConnectedToLido
|
||||
? const Color(0xFF22C55E).withOpacity(0.1)
|
||||
: const Color(0xFFEF4444).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: isConnectedToLido
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isConnectedToLido ? Icons.location_on : Icons.location_off,
|
||||
size: 40,
|
||||
color: isConnectedToLido
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isConnectedToLido
|
||||
? '📍 Connected to Lido Schenna'
|
||||
: '❌ Not at Lido Schenna',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isConnectedToLido
|
||||
return Consumer<NetworkGroupService>(
|
||||
builder: (context, networkService, child) {
|
||||
final isConnected = networkService.isConnectedToWifi;
|
||||
final networkName = networkService.currentNetworkName;
|
||||
final currentUser = networkService.currentUser;
|
||||
final networkUsers = networkService.networkUsers;
|
||||
final onlineCount = networkService.onlineUsersCount;
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFF121212),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// WiFi Connection Status Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E).withOpacity(0.1)
|
||||
: const Color(0xFFEF4444).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
isConnectedToLido
|
||||
? 'You can now vote and suggest music!'
|
||||
: 'Please visit Lido Schenna to participate',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// User Info Section
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
child: Text(
|
||||
userName.substring(0, 1),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isConnected ? Icons.wifi : Icons.wifi_off,
|
||||
size: 40,
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
userName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isConnected
|
||||
? '<EFBFBD> Connected to $networkName'
|
||||
: '❌ Not connected to WiFi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Active since 5 minutes ago',
|
||||
style: TextStyle(
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
isConnected
|
||||
? 'You can now vote and suggest music with others on this network!'
|
||||
: 'Please connect to WiFi to join the group session',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (isConnected && networkService.localIpAddress.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'IP: ${networkService.localIpAddress}',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.how_to_vote,
|
||||
color: Color(0xFF6366F1),
|
||||
size: 14,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'3 votes',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontSize: 12,
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Current User Info Section
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
child: Text(
|
||||
currentUser.name.substring(0, 1),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Active Users Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Active Guests',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${activeUsers.where((user) => user["isOnline"]).length} online',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Users List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: activeUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = activeUsers[index];
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Stack(
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
child: Text(
|
||||
user["name"].toString().substring(0, 1),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Text(
|
||||
currentUser.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (user["isOnline"])
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF22C55E),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFF121212), width: 2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Active since ${_formatDuration(DateTime.now().difference(currentUser.joinedAt))} ago',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.how_to_vote,
|
||||
color: Color(0xFF6366F1),
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${currentUser.votes} votes',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Demo Widget
|
||||
NetworkDemoWidget(networkService: networkService),
|
||||
|
||||
// Network Users Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Network Users',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'$onlineCount online',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Users List
|
||||
Expanded(
|
||||
child: isConnected
|
||||
? networkUsers.isEmpty
|
||||
? const Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
user["name"],
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
'Searching for other users...',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Joined ${user["joined"]}',
|
||||
style: const TextStyle(
|
||||
'Make sure others are connected to the same WiFi network',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: networkUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = networkUsers[index];
|
||||
final isCurrentUser = user.id == currentUser.id;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentUser
|
||||
? const Color(0xFF6366F1).withOpacity(0.1)
|
||||
: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isCurrentUser
|
||||
? Border.all(color: const Color(0xFF6366F1), width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: isCurrentUser
|
||||
? const Color(0xFF6366F1)
|
||||
: const Color(0xFF4B5563),
|
||||
child: Text(
|
||||
user.name.substring(0, 1),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.isOnline)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF22C55E),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFF121212), width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
user.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (isCurrentUser) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'You',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Joined ${_formatDuration(DateTime.now().difference(user.joinedAt))} ago',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (!user.isOnline) ...[
|
||||
const Text(
|
||||
' • ',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
const Text(
|
||||
'Offline',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (user.ipAddress.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'IP: ${user.ipAddress}',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.wifi_off,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Connect to WiFi to join a group session',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isConnected ? () async {
|
||||
// Refresh network discovery
|
||||
await networkService.refreshNetwork();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Network refreshed! ✓'),
|
||||
backgroundColor: Color(0xFF22C55E),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} : null,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Refresh Network'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${user["votes"]} votes',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isConnectedToLido ? () {
|
||||
// Refresh location/connection
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Location verified! ✓'),
|
||||
backgroundColor: Color(0xFF22C55E),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} : null,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Refresh Location'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
if (duration.inMinutes < 1) {
|
||||
return 'less than a minute';
|
||||
} else if (duration.inMinutes < 60) {
|
||||
return '${duration.inMinutes} minute${duration.inMinutes == 1 ? '' : 's'}';
|
||||
} else {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
if (minutes == 0) {
|
||||
return '$hours hour${hours == 1 ? '' : 's'}';
|
||||
} else {
|
||||
return '$hours hour${hours == 1 ? '' : 's'} $minutes minute${minutes == 1 ? '' : 's'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
475
CHALLENGE_2/sleepysound/lib/services/network_group_service.dart
Normal file
475
CHALLENGE_2/sleepysound/lib/services/network_group_service.dart
Normal file
|
@ -0,0 +1,475 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:multicast_dns/multicast_dns.dart';
|
||||
|
||||
class NetworkUser {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ipAddress;
|
||||
final DateTime joinedAt;
|
||||
final int votes;
|
||||
bool isOnline;
|
||||
DateTime lastSeen;
|
||||
|
||||
NetworkUser({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ipAddress,
|
||||
required this.joinedAt,
|
||||
this.votes = 0,
|
||||
this.isOnline = true,
|
||||
DateTime? lastSeen,
|
||||
}) : lastSeen = lastSeen ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'ipAddress': ipAddress,
|
||||
'joinedAt': joinedAt.toIso8601String(),
|
||||
'votes': votes,
|
||||
'isOnline': isOnline,
|
||||
'lastSeen': lastSeen.toIso8601String(),
|
||||
};
|
||||
|
||||
factory NetworkUser.fromJson(Map<String, dynamic> json) => NetworkUser(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
ipAddress: json['ipAddress'],
|
||||
joinedAt: DateTime.parse(json['joinedAt']),
|
||||
votes: json['votes'] ?? 0,
|
||||
isOnline: json['isOnline'] ?? true,
|
||||
lastSeen: DateTime.parse(json['lastSeen']),
|
||||
);
|
||||
}
|
||||
|
||||
class NetworkGroupService extends ChangeNotifier {
|
||||
static const int _discoveryPort = 8089;
|
||||
static const int _heartbeatInterval = 10; // seconds
|
||||
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
final NetworkInfo _networkInfo = NetworkInfo();
|
||||
MDnsClient? _mdnsClient;
|
||||
HttpServer? _httpServer;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _discoveryTimer;
|
||||
|
||||
bool _isConnectedToWifi = false;
|
||||
String _currentNetworkName = '';
|
||||
String _currentNetworkSSID = '';
|
||||
String _localIpAddress = '';
|
||||
String _networkSubnet = '';
|
||||
|
||||
final Map<String, NetworkUser> _networkUsers = {};
|
||||
late NetworkUser _currentUser;
|
||||
|
||||
// Getters
|
||||
bool get isConnectedToWifi => _isConnectedToWifi;
|
||||
String get currentNetworkName => _currentNetworkName;
|
||||
String get currentNetworkSSID => _currentNetworkSSID;
|
||||
String get localIpAddress => _localIpAddress;
|
||||
List<NetworkUser> get networkUsers => _networkUsers.values.toList();
|
||||
NetworkUser get currentUser => _currentUser;
|
||||
int get onlineUsersCount => _networkUsers.values.where((u) => u.isOnline).length;
|
||||
|
||||
NetworkGroupService() {
|
||||
_initializeCurrentUser();
|
||||
_startNetworkMonitoring();
|
||||
}
|
||||
|
||||
void _initializeCurrentUser() {
|
||||
final random = Random();
|
||||
final guestNames = ['Alex', 'Sarah', 'Marco', 'Lisa', 'Tom', 'Anna', 'David', 'Emma'];
|
||||
final randomName = guestNames[random.nextInt(guestNames.length)];
|
||||
final randomId = random.nextInt(999);
|
||||
|
||||
_currentUser = NetworkUser(
|
||||
id: 'user_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '$randomName #$randomId',
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startNetworkMonitoring() async {
|
||||
// Monitor connectivity changes
|
||||
_connectivity.onConnectivityChanged.listen(_onConnectivityChanged);
|
||||
|
||||
// Initial connectivity check
|
||||
await _checkConnectivity();
|
||||
}
|
||||
|
||||
Future<void> _onConnectivityChanged(List<ConnectivityResult> results) async {
|
||||
await _checkConnectivity();
|
||||
}
|
||||
|
||||
Future<void> _checkConnectivity() async {
|
||||
final connectivityResult = await _connectivity.checkConnectivity();
|
||||
final wasConnected = _isConnectedToWifi;
|
||||
|
||||
_isConnectedToWifi = connectivityResult.contains(ConnectivityResult.wifi);
|
||||
|
||||
if (_isConnectedToWifi) {
|
||||
await _getNetworkInfo();
|
||||
if (!wasConnected) {
|
||||
await _startNetworkServices();
|
||||
}
|
||||
} else {
|
||||
if (wasConnected) {
|
||||
await _stopNetworkServices();
|
||||
}
|
||||
_clearNetworkInfo();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getNetworkInfo() async {
|
||||
try {
|
||||
_currentNetworkSSID = await _networkInfo.getWifiName() ?? 'Unknown Network';
|
||||
_currentNetworkName = _currentNetworkSSID.replaceAll('"', ''); // Remove quotes
|
||||
_localIpAddress = await _networkInfo.getWifiIP() ?? '';
|
||||
|
||||
// Calculate network subnet (assuming /24)
|
||||
if (_localIpAddress.isNotEmpty) {
|
||||
final parts = _localIpAddress.split('.');
|
||||
if (parts.length == 4) {
|
||||
_networkSubnet = '${parts[0]}.${parts[1]}.${parts[2]}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update current user's IP
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: _currentUser.votes,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error getting network info: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _clearNetworkInfo() {
|
||||
_currentNetworkName = '';
|
||||
_currentNetworkSSID = '';
|
||||
_localIpAddress = '';
|
||||
_networkSubnet = '';
|
||||
_networkUsers.clear();
|
||||
}
|
||||
|
||||
Future<void> _startNetworkServices() async {
|
||||
if (!_isConnectedToWifi || _localIpAddress.isEmpty) return;
|
||||
|
||||
try {
|
||||
// Start mDNS client for service discovery
|
||||
_mdnsClient = MDnsClient();
|
||||
await _mdnsClient!.start();
|
||||
|
||||
// Start HTTP server for peer communication
|
||||
await _startHttpServer();
|
||||
|
||||
// Start announcing our service
|
||||
await _announceService();
|
||||
|
||||
// Start discovering other users
|
||||
_startDiscovery();
|
||||
|
||||
// Start heartbeat for keeping users online
|
||||
_startHeartbeat();
|
||||
|
||||
// Add ourselves to the users list
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error starting network services: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopNetworkServices() async {
|
||||
// Stop timers
|
||||
_heartbeatTimer?.cancel();
|
||||
_discoveryTimer?.cancel();
|
||||
|
||||
// Stop HTTP server
|
||||
await _httpServer?.close();
|
||||
_httpServer = null;
|
||||
|
||||
// Stop mDNS client
|
||||
_mdnsClient?.stop();
|
||||
_mdnsClient = null;
|
||||
|
||||
// Clear users
|
||||
_networkUsers.clear();
|
||||
}
|
||||
|
||||
Future<void> _startHttpServer() async {
|
||||
try {
|
||||
_httpServer = await HttpServer.bind(InternetAddress.anyIPv4, _discoveryPort);
|
||||
_httpServer!.listen((request) async {
|
||||
await _handleHttpRequest(request);
|
||||
});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error starting HTTP server: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleHttpRequest(HttpRequest request) async {
|
||||
try {
|
||||
final response = request.response;
|
||||
response.headers.set('Content-Type', 'application/json');
|
||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (request.method == 'GET' && request.uri.path == '/user') {
|
||||
// Return current user info
|
||||
response.write(jsonEncode(_currentUser.toJson()));
|
||||
} else if (request.method == 'POST' && request.uri.path == '/heartbeat') {
|
||||
// Handle heartbeat from other users
|
||||
final body = await utf8.decoder.bind(request).join();
|
||||
final userData = jsonDecode(body);
|
||||
final user = NetworkUser.fromJson(userData);
|
||||
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
|
||||
response.write(jsonEncode({'status': 'ok'}));
|
||||
} else {
|
||||
response.statusCode = 404;
|
||||
response.write(jsonEncode({'error': 'Not found'}));
|
||||
}
|
||||
|
||||
await response.close();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error handling HTTP request: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _announceService() async {
|
||||
if (_mdnsClient == null || _localIpAddress.isEmpty) return;
|
||||
|
||||
try {
|
||||
// This would announce our service via mDNS
|
||||
// In a real implementation, you'd use proper mDNS announcements
|
||||
if (kDebugMode) {
|
||||
print('Announcing service on $_localIpAddress:$_discoveryPort');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error announcing service: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startDiscovery() {
|
||||
_discoveryTimer = Timer.periodic(const Duration(seconds: 15), (_) async {
|
||||
await _discoverUsers();
|
||||
});
|
||||
|
||||
// Initial discovery
|
||||
_discoverUsers();
|
||||
}
|
||||
|
||||
Future<void> _discoverUsers() async {
|
||||
if (_networkSubnet.isEmpty) return;
|
||||
|
||||
// Scan local network for other SleepySound users
|
||||
final futures = <Future>[];
|
||||
|
||||
for (int i = 1; i <= 254; i++) {
|
||||
final ip = '$_networkSubnet.$i';
|
||||
if (ip != _localIpAddress) {
|
||||
futures.add(_tryConnectToUser(ip));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all connection attempts (with timeout)
|
||||
await Future.wait(futures).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => [],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _tryConnectToUser(String ip) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
|
||||
final request = await client.getUrl(Uri.parse('http://$ip:$_discoveryPort/user'));
|
||||
final response = await request.close().timeout(const Duration(seconds: 2));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = await utf8.decoder.bind(response).join();
|
||||
final userData = jsonDecode(body);
|
||||
final user = NetworkUser.fromJson(userData);
|
||||
|
||||
if (user.id != _currentUser.id) {
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// Ignore connection errors (expected for non-SleepySound devices)
|
||||
}
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: _heartbeatInterval), (_) async {
|
||||
await _sendHeartbeat();
|
||||
_cleanupOfflineUsers();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendHeartbeat() async {
|
||||
final heartbeatData = jsonEncode(_currentUser.toJson());
|
||||
|
||||
// Send heartbeat to all known users
|
||||
final futures = _networkUsers.values
|
||||
.where((user) => user.id != _currentUser.id && user.isOnline)
|
||||
.map((user) => _sendHeartbeatToUser(user.ipAddress, heartbeatData));
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
Future<void> _sendHeartbeatToUser(String ip, String data) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
|
||||
final request = await client.postUrl(Uri.parse('http://$ip:$_discoveryPort/heartbeat'));
|
||||
request.headers.set('Content-Type', 'application/json');
|
||||
request.write(data);
|
||||
|
||||
await request.close().timeout(const Duration(seconds: 2));
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// Mark user as potentially offline
|
||||
final user = _networkUsers.values.firstWhere(
|
||||
(u) => u.ipAddress == ip,
|
||||
orElse: () => NetworkUser(id: '', name: '', ipAddress: '', joinedAt: DateTime.now()),
|
||||
);
|
||||
if (user.id.isNotEmpty) {
|
||||
user.isOnline = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _cleanupOfflineUsers() {
|
||||
final now = DateTime.now();
|
||||
final usersToRemove = <String>[];
|
||||
|
||||
for (final user in _networkUsers.values) {
|
||||
if (user.id != _currentUser.id) {
|
||||
final timeSinceLastSeen = now.difference(user.lastSeen).inSeconds;
|
||||
if (timeSinceLastSeen > _heartbeatInterval * 3) {
|
||||
user.isOnline = false;
|
||||
// Remove users that have been offline for more than 5 minutes
|
||||
if (timeSinceLastSeen > 300) {
|
||||
usersToRemove.add(user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final userId in usersToRemove) {
|
||||
_networkUsers.remove(userId);
|
||||
}
|
||||
|
||||
if (usersToRemove.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for UI interaction
|
||||
Future<void> refreshNetwork() async {
|
||||
await _checkConnectivity();
|
||||
if (_isConnectedToWifi) {
|
||||
await _discoverUsers();
|
||||
}
|
||||
}
|
||||
|
||||
void updateUserVotes(int votes) {
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _currentUser.ipAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: votes,
|
||||
isOnline: true,
|
||||
);
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getConnectionStatus() {
|
||||
if (!_isConnectedToWifi) {
|
||||
return 'Not connected to WiFi';
|
||||
}
|
||||
if (_currentNetworkName.isEmpty) {
|
||||
return 'Connected to WiFi';
|
||||
}
|
||||
return 'Connected to $_currentNetworkName';
|
||||
}
|
||||
|
||||
// Demo methods for testing
|
||||
void simulateNetworkConnection() {
|
||||
_isConnectedToWifi = true;
|
||||
_currentNetworkName = 'Demo WiFi Network';
|
||||
_currentNetworkSSID = '"Demo WiFi Network"';
|
||||
_localIpAddress = '192.168.1.100';
|
||||
_networkSubnet = '192.168.1';
|
||||
|
||||
// Update current user with new IP
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: _currentUser.votes,
|
||||
);
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearDemoUsers() {
|
||||
_networkUsers.clear();
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addDemoUser(String name, int votes) {
|
||||
final user = NetworkUser(
|
||||
id: 'demo_${DateTime.now().millisecondsSinceEpoch}_${name.hashCode}',
|
||||
name: name,
|
||||
ipAddress: '192.168.1.${101 + _networkUsers.length}',
|
||||
joinedAt: DateTime.now().subtract(Duration(minutes: (votes * 2))),
|
||||
votes: votes,
|
||||
isOnline: true,
|
||||
);
|
||||
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopNetworkServices();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
139
CHALLENGE_2/sleepysound/lib/widgets/network_demo_widget.dart
Normal file
139
CHALLENGE_2/sleepysound/lib/widgets/network_demo_widget.dart
Normal file
|
@ -0,0 +1,139 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/network_group_service.dart';
|
||||
|
||||
class NetworkDemoWidget extends StatefulWidget {
|
||||
final NetworkGroupService networkService;
|
||||
|
||||
const NetworkDemoWidget({
|
||||
super.key,
|
||||
required this.networkService,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NetworkDemoWidget> createState() => _NetworkDemoWidgetState();
|
||||
}
|
||||
|
||||
class _NetworkDemoWidgetState extends State<NetworkDemoWidget> {
|
||||
bool _isDemoMode = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2D2D),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF6366F1), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.developer_mode,
|
||||
color: Color(0xFF6366F1),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Demo Mode',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF6366F1),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _isDemoMode,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isDemoMode = value;
|
||||
});
|
||||
_toggleDemoMode(value);
|
||||
},
|
||||
activeColor: const Color(0xFF6366F1),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isDemoMode) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Demo mode simulates network connectivity and adds sample users for testing.',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _addDemoUser,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1).withOpacity(0.2),
|
||||
foregroundColor: const Color(0xFF6366F1),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Add Demo User'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _simulateVote,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF22C55E).withOpacity(0.2),
|
||||
foregroundColor: const Color(0xFF22C55E),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Simulate Vote'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleDemoMode(bool enabled) {
|
||||
if (enabled) {
|
||||
// Simulate network connection
|
||||
widget.networkService.simulateNetworkConnection();
|
||||
_addInitialDemoUsers();
|
||||
} else {
|
||||
// Clear demo users
|
||||
widget.networkService.clearDemoUsers();
|
||||
}
|
||||
}
|
||||
|
||||
void _addInitialDemoUsers() {
|
||||
final demoUsers = [
|
||||
{'name': 'Alex M.', 'votes': 5},
|
||||
{'name': 'Sarah K.', 'votes': 3},
|
||||
{'name': 'Marco R.', 'votes': 7},
|
||||
{'name': 'Lisa F.', 'votes': 2},
|
||||
];
|
||||
|
||||
for (var user in demoUsers) {
|
||||
widget.networkService.addDemoUser(user['name'] as String, user['votes'] as int);
|
||||
}
|
||||
}
|
||||
|
||||
void _addDemoUser() {
|
||||
final names = ['Tom B.', 'Emma W.', 'David L.', 'Anna K.', 'Mike R.', 'Julia S.'];
|
||||
final randomName = names[DateTime.now().millisecond % names.length];
|
||||
final randomVotes = (DateTime.now().millisecond % 10) + 1;
|
||||
|
||||
widget.networkService.addDemoUser(randomName, randomVotes);
|
||||
}
|
||||
|
||||
void _simulateVote() {
|
||||
widget.networkService.updateUserVotes(widget.networkService.currentUser.votes + 1);
|
||||
}
|
||||
}
|
250
CHALLENGE_2/sleepysound/lib/widgets/network_widgets.dart
Normal file
250
CHALLENGE_2/sleepysound/lib/widgets/network_widgets.dart
Normal file
|
@ -0,0 +1,250 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/network_group_service.dart';
|
||||
|
||||
class NetworkStatusCard extends StatelessWidget {
|
||||
final NetworkGroupService networkService;
|
||||
|
||||
const NetworkStatusCard({
|
||||
super.key,
|
||||
required this.networkService,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = networkService.isConnectedToWifi;
|
||||
final networkName = networkService.currentNetworkName;
|
||||
final localIp = networkService.localIpAddress;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E).withOpacity(0.1)
|
||||
: const Color(0xFFEF4444).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isConnected ? Icons.wifi : Icons.wifi_off,
|
||||
size: 40,
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
isConnected
|
||||
? '📶 Connected to $networkName'
|
||||
: '❌ Not connected to WiFi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isConnected
|
||||
? const Color(0xFF22C55E)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
isConnected
|
||||
? 'You can now vote and suggest music with others on this network!'
|
||||
: 'Please connect to WiFi to join the group session',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (isConnected && localIp.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your IP: $localIp',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserCard extends StatelessWidget {
|
||||
final NetworkUser user;
|
||||
final bool isCurrentUser;
|
||||
|
||||
const UserCard({
|
||||
super.key,
|
||||
required this.user,
|
||||
this.isCurrentUser = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentUser
|
||||
? const Color(0xFF6366F1).withOpacity(0.1)
|
||||
: const Color(0xFF1E1E1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isCurrentUser
|
||||
? Border.all(color: const Color(0xFF6366F1), width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: isCurrentUser
|
||||
? const Color(0xFF6366F1)
|
||||
: const Color(0xFF4B5563),
|
||||
child: Text(
|
||||
user.name.substring(0, 1),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.isOnline)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF22C55E),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFF121212), width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
user.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (isCurrentUser) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6366F1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'You',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Joined ${_formatDuration(DateTime.now().difference(user.joinedAt))} ago',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (!user.isOnline) ...[
|
||||
const Text(
|
||||
' • ',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
const Text(
|
||||
'Offline',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (user.ipAddress.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'IP: ${user.ipAddress}',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
if (duration.inMinutes < 1) {
|
||||
return 'less than a minute';
|
||||
} else if (duration.inMinutes < 60) {
|
||||
return '${duration.inMinutes} minute${duration.inMinutes == 1 ? '' : 's'}';
|
||||
} else {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
if (minutes == 0) {
|
||||
return '$hours hour${hours == 1 ? '' : 's'}';
|
||||
} else {
|
||||
return '$hours hour${hours == 1 ? '' : 's'} $minutes minute${minutes == 1 ? '' : 's'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,11 +5,15 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import connectivity_plus
|
||||
import network_info_plus
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
|
|
|
@ -145,6 +145,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -177,6 +193,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -384,6 +408,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
multicast_dns:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: multicast_dns
|
||||
sha256: de72ada5c3db6fdd6ad4ae99452fe05fb403c4bb37c67ceb255ddd37d2b5b1eb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -392,6 +424,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
network_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: network_info_plus
|
||||
sha256: "5bd4b86e28fed5ed4e6ac7764133c031dfb7d3f46aa2a81b46f55038aa78ecc0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
network_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: network_info_plus_platform_interface
|
||||
sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -432,6 +488,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -789,6 +853,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.22.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -797,6 +869,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -52,6 +52,13 @@ dependencies:
|
|||
|
||||
# State management
|
||||
provider: ^6.1.1
|
||||
|
||||
# Network and WiFi connectivity
|
||||
connectivity_plus: ^6.0.5
|
||||
network_info_plus: ^5.0.3
|
||||
|
||||
# Local network discovery
|
||||
multicast_dns: ^0.3.2+4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue