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">
|
<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
|
<application
|
||||||
android:label="sleepysound"
|
android:label="sleepysound"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'pages/now_playing_page.dart';
|
||||||
import 'pages/voting_page.dart';
|
import 'pages/voting_page.dart';
|
||||||
import 'pages/group_page.dart';
|
import 'pages/group_page.dart';
|
||||||
import 'services/music_queue_service.dart';
|
import 'services/music_queue_service.dart';
|
||||||
|
import 'services/network_group_service.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
@ -15,8 +16,11 @@ class MyApp extends StatelessWidget {
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return MultiProvider(
|
||||||
create: (context) => MusicQueueService(),
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (context) => MusicQueueService()),
|
||||||
|
ChangeNotifierProvider(create: (context) => NetworkGroupService()),
|
||||||
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'SleepySound',
|
title: 'SleepySound',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/music_queue_service.dart';
|
import '../services/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});
|
||||||
|
@ -10,314 +11,456 @@ class GroupPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GroupPageState extends State<GroupPage> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Consumer<NetworkGroupService>(
|
||||||
color: const Color(0xFF121212),
|
builder: (context, networkService, child) {
|
||||||
child: Padding(
|
final isConnected = networkService.isConnectedToWifi;
|
||||||
padding: const EdgeInsets.all(20.0),
|
final networkName = networkService.currentNetworkName;
|
||||||
child: Column(
|
final currentUser = networkService.currentUser;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final networkUsers = networkService.networkUsers;
|
||||||
children: [
|
final onlineCount = networkService.onlineUsersCount;
|
||||||
// Location Status Card
|
|
||||||
Container(
|
return Container(
|
||||||
width: double.infinity,
|
color: const Color(0xFF121212),
|
||||||
padding: const EdgeInsets.all(20),
|
child: Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(20.0),
|
||||||
color: isConnectedToLido
|
child: Column(
|
||||||
? const Color(0xFF22C55E).withOpacity(0.1)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
: const Color(0xFFEF4444).withOpacity(0.1),
|
children: [
|
||||||
borderRadius: BorderRadius.circular(15),
|
// WiFi Connection Status Card
|
||||||
border: Border.all(
|
Container(
|
||||||
color: isConnectedToLido
|
width: double.infinity,
|
||||||
? const Color(0xFF22C55E)
|
padding: const EdgeInsets.all(20),
|
||||||
: const Color(0xFFEF4444),
|
decoration: BoxDecoration(
|
||||||
width: 1,
|
color: isConnected
|
||||||
),
|
? const Color(0xFF22C55E).withOpacity(0.1)
|
||||||
),
|
: const Color(0xFFEF4444).withOpacity(0.1),
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(15),
|
||||||
children: [
|
border: Border.all(
|
||||||
Icon(
|
color: isConnected
|
||||||
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
|
|
||||||
? const Color(0xFF22C55E)
|
? const Color(0xFF22C55E)
|
||||||
: const Color(0xFFEF4444),
|
: const Color(0xFFEF4444),
|
||||||
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
child: Column(
|
||||||
Text(
|
children: [
|
||||||
isConnectedToLido
|
Icon(
|
||||||
? 'You can now vote and suggest music!'
|
isConnected ? Icons.wifi : Icons.wifi_off,
|
||||||
: 'Please visit Lido Schenna to participate',
|
size: 40,
|
||||||
style: const TextStyle(
|
color: isConnected
|
||||||
color: Colors.grey,
|
? const Color(0xFF22C55E)
|
||||||
fontSize: 14,
|
: const Color(0xFFEF4444),
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
),
|
Text(
|
||||||
const SizedBox(width: 15),
|
isConnected
|
||||||
Expanded(
|
? '<EFBFBD> Connected to $networkName'
|
||||||
child: Column(
|
: '❌ Not connected to WiFi',
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 18,
|
||||||
Text(
|
fontWeight: FontWeight.bold,
|
||||||
userName,
|
color: isConnected
|
||||||
style: const TextStyle(
|
? const Color(0xFF22C55E)
|
||||||
color: Colors.white,
|
: const Color(0xFFEF4444),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
const Text(
|
const SizedBox(height: 5),
|
||||||
'Active since 5 minutes ago',
|
Text(
|
||||||
style: TextStyle(
|
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,
|
color: Colors.grey,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
fontFamily: 'monospace',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
const SizedBox(height: 25),
|
||||||
color: const Color(0xFF6366F1).withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
// Current User Info Section
|
||||||
),
|
Container(
|
||||||
child: const Row(
|
width: double.infinity,
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.all(18),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
Icon(
|
color: const Color(0xFF1E1E1E),
|
||||||
Icons.how_to_vote,
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: Color(0xFF6366F1),
|
),
|
||||||
size: 14,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
SizedBox(width: 4),
|
CircleAvatar(
|
||||||
Text(
|
radius: 25,
|
||||||
'3 votes',
|
backgroundColor: const Color(0xFF6366F1),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
color: Color(0xFF6366F1),
|
currentUser.name.substring(0, 1),
|
||||||
fontSize: 12,
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(width: 15),
|
||||||
),
|
Expanded(
|
||||||
],
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
|
||||||
|
|
||||||
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(
|
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
Text(
|
||||||
radius: 20,
|
currentUser.name,
|
||||||
backgroundColor: const Color(0xFF6366F1),
|
style: const TextStyle(
|
||||||
child: Text(
|
color: Colors.white,
|
||||||
user["name"].toString().substring(0, 1),
|
fontSize: 16,
|
||||||
style: const TextStyle(
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (user["isOnline"])
|
const SizedBox(height: 4),
|
||||||
Positioned(
|
Text(
|
||||||
right: 0,
|
'Active since ${_formatDuration(DateTime.now().difference(currentUser.joinedAt))} ago',
|
||||||
bottom: 0,
|
style: const TextStyle(
|
||||||
child: Container(
|
color: Colors.grey,
|
||||||
width: 12,
|
fontSize: 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(
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
user["name"],
|
'Searching for other users...',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Joined ${user["joined"]}',
|
'Make sure others are connected to the same WiFi network',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontSize: 12,
|
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),
|
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 FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import connectivity_plus
|
||||||
|
import network_info_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
|
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
|
|
|
@ -145,6 +145,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
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:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -177,6 +193,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -384,6 +408,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -392,6 +424,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -432,6 +488,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -789,6 +853,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.22.1"
|
version: "3.22.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -797,6 +869,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -52,6 +52,13 @@ dependencies:
|
||||||
|
|
||||||
# State management
|
# State management
|
||||||
provider: ^6.1.1
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
@ -6,9 +6,12 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
connectivity_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue