nearby 0.1.0 copy "nearby: ^0.1.0" to clipboard
nearby: ^0.1.0 copied to clipboard

Peer-to-peer connectivity across Wi-Fi and Bluetooth

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:nearby/nearby.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

import 'broadcast_demo.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const NearbyExampleApp());
}

class NearbyExampleApp extends StatelessWidget {
  const NearbyExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Nearby Connectivity Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF6366F1), // Indigo accent
          secondary: Color(0xFF10B981), // Emerald accent
          surface: Color(0xFF1E1E2E),
        ),
        scaffoldBackgroundColor: const Color(0xFF11111B),
        cardColor: const Color(0xFF1E1E2E),
        useMaterial3: true,
      ),
      home: const NearbyHomeScreen(),
    );
  }
}

class NearbyHomeScreen extends StatefulWidget {
  const NearbyHomeScreen({super.key});

  @override
  State<NearbyHomeScreen> createState() => _NearbyHomeScreenState();
}

class _NearbyHomeScreenState extends State<NearbyHomeScreen> {
  late final NearbyService _nearbyService;
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _msgController = TextEditingController();
  final ScrollController _chatScrollController = ScrollController();

  DiscoveryStrategy _selectedStrategy = DiscoveryStrategy.hybrid;
  SecurityMode _securityMode = SecurityMode.pinVerification;
  final String _serviceId = 'nearby-demo';

  List<Peer> _discoveredPeers = [];
  final List<String> _chatMessages = [];
  final Map<int, PayloadTransferUpdate> _activeTransfers = {};
  final Map<String, String?> _pendingConnections = {};

  StreamSubscription? _discoverySub;
  StreamSubscription? _requestSub;
  StreamSubscription? _stateSub;
  StreamSubscription? _payloadSub;
  StreamSubscription? _progressSub;

  StreamController<List<int>>? _activeSensorStream;
  Timer? _sensorTimer;
  int _sensorCounter = 0;
  String? _selectedPeerId;
  Directory? _documentsDir;

  @override
  void initState() {
    super.initState();
    _initNearbyService();
    _initStorageDir();
    _requestPermissions();
  }

  void _initNearbyService() {
    _nearbyService = NearbyService(
      localDisplayName: 'Flutter Device ${DateTime.now().millisecond}',
    );
    _nameController.text = _nearbyService.localDisplayName;

    // Listen to discovered peers
    _discoverySub = _nearbyService.discoveredPeersStream.listen((peers) {
      if (mounted) {
        setState(() {
          _discoveredPeers = peers;
        });
      }
    });

    // Listen to incoming connection requests
    _requestSub = _nearbyService.connectionRequestsStream.listen((request) {
      if (mounted) {
        _showIncomingRequestDialog(request);
      }
    });

    // Listen to peer connection state changes
    _stateSub = _nearbyService.peerStateStream.listen((update) {
      if (mounted) {
        setState(() {
          if (update.state == PeerConnectionState.authenticating) {
            _pendingConnections[update.peer.id] = update.sasPin;
          } else if (update.state == PeerConnectionState.connected) {
            _pendingConnections.remove(update.peer.id);
            _selectedPeerId ??= update.peer.id;
          } else if (update.state == PeerConnectionState.disconnected) {
            _pendingConnections.remove(update.peer.id);
            if (_selectedPeerId == update.peer.id) {
              final remaining = _nearbyService.connectedPeers;
              _selectedPeerId = remaining.isNotEmpty
                  ? remaining.first.id
                  : null;
            }
          }
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              '${update.peer.displayName}: ${update.state.name.toUpperCase()}${update.sasPin != null ? ' (PIN: ${update.sasPin})' : ''}',
            ),
            duration: const Duration(seconds: 2),
          ),
        );
      }
    });

    // Listen to received payloads
    _payloadSub = _nearbyService.payloadReceivedStream.listen((payload) {
      if (payload.type == PayloadType.bytes && payload.bytes != null) {
        final text = utf8.decode(payload.bytes!, allowMalformed: true);
        if (mounted) {
          setState(() {
            _chatMessages.add('📥 Remote: $text');
          });
          _scrollToBottom();
        }
      } else if (payload.type == PayloadType.file && payload.file != null) {
        if (mounted) {
          setState(() {
            _chatMessages.add(
              '📁 Received File: ${payload.fileName} (${payload.totalBytes} bytes)',
            );
          });
          _scrollToBottom();
        }
      } else if (payload.type == PayloadType.stream && payload.stream != null) {
        if (mounted) {
          setState(() {
            _chatMessages.add('🌊 Receiving continuous live stream...');
          });
        }
        payload.stream!.listen((data) {
          // Stream data chunk received
        });
      }
    });

    // Listen to transfer progress
    _progressSub = _nearbyService.payloadProgressStream.listen((update) {
      if (mounted) {
        setState(() {
          _activeTransfers[update.payloadId] = update;
          if (update.status == PayloadStatus.success ||
              update.status == PayloadStatus.canceled ||
              update.status == PayloadStatus.failure) {
            Future.delayed(const Duration(seconds: 4), () {
              if (mounted) {
                setState(() {
                  _activeTransfers.remove(update.payloadId);
                });
              }
            });
          }
        });
      }
    });
  }

  Future<void> _initStorageDir() async {
    try {
      final dir = await getApplicationDocumentsDirectory();
      if (mounted) {
        setState(() {
          _documentsDir = dir;
          _nearbyService.storageDirectory = dir;
        });
      }
    } catch (e) {
      debugPrint('Failed to get application documents directory: $e');
    }
  }

  Future<void> _requestPermissions() async {
    if (Platform.isIOS) {
      await [Permission.bluetooth].request();
    } else if (Platform.isAndroid) {
      await [
        Permission.bluetoothScan,
        Permission.bluetoothAdvertise,
        Permission.bluetoothConnect,
        Permission.locationWhenInUse,
        Permission.nearbyWifiDevices,
      ].request();
    }
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_chatScrollController.hasClients) {
        _chatScrollController.animateTo(
          _chatScrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOut,
        );
      }
    });
  }

  Future<void> _toggleAdvertising() async {
    if (_nearbyService.isAdvertising) {
      await _nearbyService.stopAdvertising();
    } else {
      await _nearbyService.startAdvertising(
        options: AdvertisingOptions(
          serviceId: _serviceId,
          strategy: _selectedStrategy,
          securityMode: _securityMode,
        ),
      );
    }
    if (mounted) setState(() {});
  }

  Future<void> _toggleDiscovery() async {
    if (_nearbyService.isDiscovering) {
      await _nearbyService.stopDiscovery();
    } else {
      await _nearbyService.startDiscovery(
        options: DiscoveryOptions(
          serviceId: _serviceId,
          strategy: _selectedStrategy,
        ),
      );
    }
    if (mounted) setState(() {});
  }

  void _showIncomingRequestDialog(ConnectionRequest request) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (ctx) => AlertDialog(
        backgroundColor: const Color(0xFF1E1E2E),
        title: const Text('Incoming Connection'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Peer: ${request.peer.displayName}'),
            const SizedBox(height: 8),
            Text('Transport: ${request.peer.discoveredVia.name.toUpperCase()}'),
            const SizedBox(height: 16),
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: const Color(0xFF282A36),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: const Color(0xFF6366F1)),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text(
                    'Verification PIN: ',
                    style: TextStyle(color: Colors.grey),
                  ),
                  Text(
                    request.authenticationPin,
                    style: const TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                      letterSpacing: 4,
                      color: Color(0xFF10B981),
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 8),
            const Text(
              'Verify that the other device displays the same PIN.',
              style: TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(ctx);
              _nearbyService.rejectConnection(
                request.peer.id,
                reason: 'Declined by user',
              );
            },
            child: const Text(
              'Reject',
              style: TextStyle(color: Colors.redAccent),
            ),
          ),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: const Color(0xFF10B981),
            ),
            onPressed: () {
              Navigator.pop(ctx);
              _nearbyService.acceptConnection(request.peer.id);
            },
            child: const Text(
              'Accept & Connect',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _connectToPeer(Peer peer) async {
    setState(() {
      _pendingConnections[peer.id] = null;
    });

    try {
      final success = await _nearbyService.requestConnection(peer);
      if (!success && mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              'Connection to ${peer.displayName} was declined or timed out.',
            ),
            backgroundColor: Colors.redAccent,
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Error connecting to ${peer.displayName}: $e'),
            backgroundColor: Colors.redAccent,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _pendingConnections.remove(peer.id);
        });
      }
    }
  }

  void _sendMessage() {
    final text = _msgController.text.trim();
    if (text.isEmpty || _selectedPeerId == null) return;

    final bytes = Uint8List.fromList(utf8.encode(text));
    _nearbyService.sendBytes(_selectedPeerId!, bytes);

    setState(() {
      _chatMessages.add('📤 You: $text');
    });
    _msgController.clear();
    _scrollToBottom();
  }

  Future<void> _sendSampleFile() async {
    if (_selectedPeerId == null || _documentsDir == null) return;

    final sampleFile = File('${_documentsDir!.path}/sample_photo.bin');
    final sampleBytes = Uint8List.fromList(
      List.generate(256 * 1024, (i) => (i * 7) % 256),
    );
    sampleFile.writeAsBytesSync(sampleBytes);

    await _nearbyService.sendFile(
      _selectedPeerId!,
      sampleFile,
      customFileName: 'nearby_test_image.jpg',
    );

    setState(() {
      _chatMessages.add('📤 Sent File: nearby_test_image.jpg (256 KB)');
    });
    _scrollToBottom();
  }

  void _toggleLiveStream() {
    if (_selectedPeerId == null) return;

    if (_activeSensorStream != null) {
      _sensorTimer?.cancel();
      _sensorTimer = null;
      _activeSensorStream?.close();
      _activeSensorStream = null;
      setState(() {
        _chatMessages.add('⏹ Live sensor stream stopped');
      });
    } else {
      _activeSensorStream = StreamController<List<int>>.broadcast();
      _nearbyService.sendStream(_selectedPeerId!, _activeSensorStream!.stream);

      _sensorCounter = 0;
      _sensorTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
        _sensorCounter++;
        final data = utf8.encode(
          'Tick #$_sensorCounter (Sensors: X=${_sensorCounter % 10}, Y=${_sensorCounter * 2 % 100})',
        );
        _activeSensorStream?.add(data);
      });

      setState(() {
        _chatMessages.add('▶ Streaming live sensor telemetry...');
      });
    }
    _scrollToBottom();
  }

  @override
  void dispose() {
    _sensorTimer?.cancel();
    _activeSensorStream?.close();
    _discoverySub?.cancel();
    _requestSub?.cancel();
    _stateSub?.cancel();
    _payloadSub?.cancel();
    _progressSub?.cancel();
    unawaited(_nearbyService.dispose());
    _nameController.dispose();
    _msgController.dispose();
    _chatScrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final connectedPeers = _nearbyService.connectedPeers;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Nearby Cross-Platform Sync'),
        backgroundColor: const Color(0xFF1E1E2E),
        elevation: 0,
        actions: [
          IconButton(
            tooltip: 'Broadcast demo',
            icon: const Icon(Icons.campaign_outlined),
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (_) => const BroadcastDemoScreen(),
              ),
            ),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Control Card
            _buildControlCard(),
            const SizedBox(height: 16),

            // Discovered Peers
            _buildDiscoveredPeersCard(),
            const SizedBox(height: 16),

            // Connected Sessions & Data Transfer
            if (connectedPeers.isNotEmpty) ...[
              _buildConnectedSessionCard(connectedPeers),
              const SizedBox(height: 16),
              _buildDataTransferCard(),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildControlCard() {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Configuration & Controls',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _nameController,
              decoration: const InputDecoration(
                labelText: 'Display Name',
                border: OutlineInputBorder(),
                isDense: true,
              ),
            ),
            const SizedBox(height: 12),
            DropdownButtonFormField<DiscoveryStrategy>(
              initialValue: _selectedStrategy,
              isExpanded: true,
              decoration: const InputDecoration(
                labelText: 'Discovery Medium',
                border: OutlineInputBorder(),
                isDense: true,
              ),
              items: const [
                DropdownMenuItem(
                  value: DiscoveryStrategy.hybrid,
                  child: Text(
                    'Hybrid (mDNS + BLE)',
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                DropdownMenuItem(
                  value: DiscoveryStrategy.networkOnly,
                  child: Text(
                    'mDNS Only (LAN / Wi-Fi)',
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                DropdownMenuItem(
                  value: DiscoveryStrategy.bleOnly,
                  child: Text('BLE Only', overflow: TextOverflow.ellipsis),
                ),
              ],
              onChanged: (val) {
                if (val != null) setState(() => _selectedStrategy = val);
              },
            ),
            const SizedBox(height: 12),
            DropdownButtonFormField<SecurityMode>(
              initialValue: _securityMode,
              isExpanded: true,
              decoration: const InputDecoration(
                labelText: 'Security Handshake',
                border: OutlineInputBorder(),
                isDense: true,
              ),
              items: const [
                DropdownMenuItem(
                  value: SecurityMode.pinVerification,
                  child: Text(
                    'PIN / SAS Verification (Recommended)',
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                DropdownMenuItem(
                  value: SecurityMode.autoAccept,
                  child: Text(
                    'Auto-Accept (Insecure)',
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
              onChanged: (val) {
                if (val != null) setState(() => _securityMode = val);
              },
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _nearbyService.isAdvertising
                          ? Colors.redAccent
                          : const Color(0xFF6366F1),
                      padding: const EdgeInsets.symmetric(
                        vertical: 12,
                        horizontal: 8,
                      ),
                    ),
                    icon: Icon(
                      _nearbyService.isAdvertising
                          ? Icons.stop
                          : Icons.cell_tower,
                      color: Colors.white,
                      size: 20,
                    ),
                    label: FittedBox(
                      fit: BoxFit.scaleDown,
                      child: Text(
                        _nearbyService.isAdvertising
                            ? 'Stop Advertising'
                            : 'Start Advertising',
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    onPressed: _toggleAdvertising,
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton.icon(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _nearbyService.isDiscovering
                          ? Colors.orangeAccent
                          : const Color(0xFF10B981),
                      padding: const EdgeInsets.symmetric(
                        vertical: 12,
                        horizontal: 8,
                      ),
                    ),
                    icon: Icon(
                      _nearbyService.isDiscovering ? Icons.stop : Icons.radar,
                      color: Colors.white,
                      size: 20,
                    ),
                    label: FittedBox(
                      fit: BoxFit.scaleDown,
                      child: Text(
                        _nearbyService.isDiscovering
                            ? 'Stop Discovery'
                            : 'Start Discovery',
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    onPressed: _toggleDiscovery,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDiscoveredPeersCard() {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Expanded(
                  child: Text(
                    'Discovered Nearby Peers',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                const SizedBox(width: 8),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10,
                    vertical: 4,
                  ),
                  decoration: BoxDecoration(
                    color: const Color(0xFF282A36),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    '${_discoveredPeers.length} Found',
                    style: const TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
              ],
            ),
            if (_pendingConnections.isNotEmpty) ...[
              const SizedBox(height: 10),
              ..._pendingConnections.entries.map((entry) {
                final peerId = entry.key;
                final pin = entry.value;
                final peer = _discoveredPeers.firstWhere(
                  (p) => p.id == peerId,
                  orElse: () => Peer(
                    id: peerId,
                    displayName: 'Remote Peer',
                    discoveredVia: DiscoveryMedium.ble,
                    lastSeen: DateTime.now(),
                  ),
                );

                return Container(
                  margin: const EdgeInsets.only(bottom: 8),
                  padding: const EdgeInsets.all(12),
                  decoration: BoxDecoration(
                    color: const Color(0xFF1E1B4B),
                    borderRadius: BorderRadius.circular(10),
                    border: Border.all(color: const Color(0xFF6366F1)),
                  ),
                  child: Row(
                    children: [
                      const SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2.5,
                          color: Color(0xFF10B981),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              'Connecting to ${peer.displayName}...',
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(height: 2),
                            Text(
                              pin != null
                                  ? 'Verify PIN: $pin on other device'
                                  : 'Negotiating connection...',
                              style: TextStyle(
                                fontSize: 13,
                                color: pin != null
                                    ? const Color(0xFF10B981)
                                    : Colors.grey,
                                fontWeight: pin != null
                                    ? FontWeight.bold
                                    : FontWeight.normal,
                              ),
                            ),
                          ],
                        ),
                      ),
                      if (pin != null)
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 10,
                            vertical: 4,
                          ),
                          decoration: BoxDecoration(
                            color: const Color(0xFF10B981)
                                .withValues(alpha: 0.2),
                            borderRadius: BorderRadius.circular(6),
                            border: Border.all(color: const Color(0xFF10B981)),
                          ),
                          child: Text(
                            pin,
                            style: const TextStyle(
                              fontSize: 18,
                              fontWeight: FontWeight.bold,
                              letterSpacing: 2,
                              color: Color(0xFF10B981),
                            ),
                          ),
                        ),
                    ],
                  ),
                );
              }),
            ],
            const SizedBox(height: 8),
            if (_discoveredPeers.isEmpty && _pendingConnections.isEmpty)
              const Padding(
                padding: EdgeInsets.symmetric(vertical: 24.0),
                child: Center(
                  child: Text(
                    'No nearby peers discovered yet. Start discovery or advertising on other devices.',
                    style: TextStyle(color: Colors.grey),
                    textAlign: TextAlign.center,
                  ),
                ),
              )
            else if (_discoveredPeers.isNotEmpty)
              ListView.separated(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                itemCount: _discoveredPeers.length,
                separatorBuilder: (_, _) =>
                    const Divider(color: Colors.white10),
                itemBuilder: (ctx, idx) {
                  final peer = _discoveredPeers[idx];
                  return ListTile(
                    contentPadding: EdgeInsets.zero,
                    leading: CircleAvatar(
                      backgroundColor: const Color(0xFF6366F1),
                      child: Icon(
                        peer.discoveredVia == DiscoveryMedium.network
                            ? Icons.wifi
                            : (peer.discoveredVia == DiscoveryMedium.ble
                                  ? Icons.bluetooth
                                  : Icons.devices),
                        color: Colors.white,
                      ),
                    ),
                    title: Text(
                      peer.displayName,
                      style: const TextStyle(fontWeight: FontWeight.w600),
                      overflow: TextOverflow.ellipsis,
                    ),
                    subtitle: Text(
                      'Via: ${peer.discoveredVia.name.toUpperCase()}${peer.ipAddress != null ? ' | IP: ${peer.ipAddress}' : ''}',
                      style: const TextStyle(fontSize: 12, color: Colors.grey),
                      overflow: TextOverflow.ellipsis,
                    ),
                    trailing: _buildPeerTrailingAction(peer),
                  );
                },
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildPeerTrailingAction(Peer peer) {
    final isPending = _pendingConnections.containsKey(peer.id);
    if (!isPending) {
      return ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFF6366F1),
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
        ),
        onPressed: () => _connectToPeer(peer),
        child: const Text('Connect', style: TextStyle(color: Colors.white)),
      );
    }

    final pin = _pendingConnections[peer.id];
    if (pin != null) {
      // Authenticating / Waiting for user to verify PIN on remote peer
      return Container(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
        decoration: BoxDecoration(
          color: const Color(0xFF1E1B4B),
          borderRadius: BorderRadius.circular(8),
          border: Border.all(color: const Color(0xFF6366F1)),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            const SizedBox(
              width: 14,
              height: 14,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                color: Color(0xFF10B981),
              ),
            ),
            const SizedBox(width: 8),
            Text(
              'PIN: $pin',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                color: Color(0xFF10B981),
                fontSize: 13,
                letterSpacing: 1.5,
              ),
            ),
          ],
        ),
      );
    }

    // Initial connecting / negotiating handshake
    return ElevatedButton.icon(
      style: ElevatedButton.styleFrom(
        backgroundColor: const Color(0xFF374151),
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
      ),
      onPressed: null,
      icon: const SizedBox(
        width: 14,
        height: 14,
        child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white70),
      ),
      label: const Text(
        'Connecting...',
        style: TextStyle(color: Colors.white70, fontSize: 13),
      ),
    );
  }

  Future<void> _disconnectPeer(Peer peer) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        backgroundColor: const Color(0xFF1E1E2E),
        title: const Text('Disconnect Peer'),
        content: Text(
          'Are you sure you want to disconnect from ${peer.displayName}?',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, false),
            child: const Text('Cancel', style: TextStyle(color: Colors.grey)),
          ),
          ElevatedButton(
            style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
            onPressed: () => Navigator.pop(ctx, true),
            child: const Text(
              'Disconnect',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );

    if (confirmed == true) {
      await _nearbyService.disconnect(peer.id, reason: 'Disconnected by user');
      if (mounted) {
        setState(() {
          if (_selectedPeerId == peer.id) {
            final remaining = _nearbyService.connectedPeers
                .where((p) => p.id != peer.id)
                .toList();
            _selectedPeerId = remaining.isNotEmpty ? remaining.first.id : null;
          }
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Disconnected from ${peer.displayName}'),
            duration: const Duration(seconds: 2),
          ),
        );
      }
    }
  }

  Widget _buildConnectedSessionCard(List<Peer> connectedPeers) {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'Connected Peers',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                if (connectedPeers.length > 1)
                  TextButton.icon(
                    style: TextButton.styleFrom(
                      foregroundColor: Colors.redAccent,
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                        vertical: 4,
                      ),
                    ),
                    icon: const Icon(Icons.link_off, size: 16),
                    label: const Text(
                      'Disconnect All',
                      style: TextStyle(fontSize: 12),
                    ),
                    onPressed: () async {
                      await _nearbyService.disconnectAll(
                        reason: 'Disconnected all peers',
                      );
                      if (mounted) {
                        setState(() {
                          _selectedPeerId = null;
                        });
                      }
                    },
                  ),
              ],
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: connectedPeers.map((p) {
                final isSelected = p.id == _selectedPeerId;
                return InputChip(
                  avatar: Icon(
                    p.discoveredVia == DiscoveryMedium.network
                        ? Icons.wifi
                        : (p.discoveredVia == DiscoveryMedium.ble
                              ? Icons.bluetooth
                              : Icons.devices),
                    size: 16,
                    color: isSelected ? Colors.white : Colors.grey,
                  ),
                  label: Text(
                    p.displayName,
                    style: TextStyle(
                      color: isSelected ? Colors.white : Colors.white70,
                      fontWeight: isSelected
                          ? FontWeight.bold
                          : FontWeight.normal,
                    ),
                  ),
                  selected: isSelected,
                  selectedColor: const Color(0xFF6366F1),
                  backgroundColor: const Color(0xFF282A36),
                  onSelected: (selected) {
                    if (selected) setState(() => _selectedPeerId = p.id);
                  },
                  deleteIcon: const Icon(
                    Icons.close,
                    size: 16,
                    color: Colors.white70,
                  ),
                  deleteButtonTooltipMessage: 'Disconnect',
                  onDeleted: () => _disconnectPeer(p),
                );
              }).toList(),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDataTransferCard() {
    return Card(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Data Transfer Workspace',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),

            // Active Progress Indicators
            if (_activeTransfers.isNotEmpty) ...[
              ..._activeTransfers.values.map((u) {
                return Padding(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Expanded(
                            child: Text(
                              'Payload #${u.payloadId} (${u.status.name})',
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                          const SizedBox(width: 8),
                          Text(
                            '${u.percentage}% (${u.bytesTransferred}/${u.totalBytes} B)',
                            style: const TextStyle(
                              fontSize: 12,
                              color: Colors.grey,
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 4),
                      LinearProgressIndicator(
                        value: u.progress,
                        backgroundColor: Colors.white10,
                        color: const Color(0xFF10B981),
                      ),
                    ],
                  ),
                );
              }),
              const Divider(color: Colors.white10),
            ],

            // Action Buttons for File and Stream
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    style: OutlinedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        vertical: 10,
                        horizontal: 8,
                      ),
                    ),
                    icon: const Icon(Icons.upload_file, size: 18),
                    label: const FittedBox(
                      fit: BoxFit.scaleDown,
                      child: Text('Send 256KB File'),
                    ),
                    onPressed: _sendSampleFile,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton.icon(
                    style: OutlinedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        vertical: 10,
                        horizontal: 8,
                      ),
                    ),
                    icon: Icon(
                      _activeSensorStream != null ? Icons.stop : Icons.sensors,
                      color: _activeSensorStream != null
                          ? Colors.redAccent
                          : Colors.white,
                      size: 18,
                    ),
                    label: FittedBox(
                      fit: BoxFit.scaleDown,
                      child: Text(
                        _activeSensorStream != null
                            ? 'Stop Stream'
                            : 'Live Stream',
                      ),
                    ),
                    onPressed: _toggleLiveStream,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),

            // Chat Message Box
            Container(
              height: 180,
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: const Color(0xFF11111B),
                borderRadius: BorderRadius.circular(8),
              ),
              child: ListView.builder(
                controller: _chatScrollController,
                itemCount: _chatMessages.length,
                itemBuilder: (ctx, i) => Padding(
                  padding: const EdgeInsets.symmetric(vertical: 2),
                  child: Text(
                    _chatMessages[i],
                    style: TextStyle(
                      color: _chatMessages[i].startsWith('📤')
                          ? const Color(0xFF6366F1)
                          : const Color(0xFF10B981),
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 8),

            // Message Input
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _msgController,
                    decoration: const InputDecoration(
                      hintText: 'Type a message...',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  icon: const Icon(Icons.send),
                  onPressed: _sendMessage,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}