import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../services/call_session.dart'; import '../widgets/level_meter.dart'; import '../widgets/status_indicator.dart'; /// Active-call screen with push-to-talk interface. /// /// Layout: /// ┌─────────────────────────┐ /// │ Status indicator │ /// │ TX / RX level meters │ /// │ Stats (frames, errors) │ /// │ [ PTT BUTTON ] │ /// │ [Listen] [End Call] │ /// └─────────────────────────┘ class CallScreen extends StatefulWidget { final CallSession session; const CallScreen({super.key, required this.session}); @override State createState() => _CallScreenState(); } class _CallScreenState extends State { StreamSubscription? _stateSub; Timer? _statsTimer; SessionState _state = SessionState.inCall; bool _pttDown = false; bool _listening = false; // Stats (refreshed at 4 Hz). int _txFrames = 0; int _rxFrames = 0; int _rxErrors = 0; double _txLevel = 0.0; double _rxLevel = 0.0; @override void initState() { super.initState(); _state = widget.session.state; _stateSub = widget.session.onStateChange.listen((s) { if (mounted) setState(() => _state = s); }); // Refresh stats at 4 Hz. _statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) { if (mounted) { setState(() { _txFrames = widget.session.txFrames; _rxFrames = widget.session.rxFrames; _rxErrors = widget.session.rxErrors; _txLevel = (widget.session.txRms * 4).clamp(0.0, 1.0); _rxLevel = (widget.session.rxSignalRms * 4).clamp(0.0, 1.0); }); } }); } @override void dispose() { _stateSub?.cancel(); _statsTimer?.cancel(); super.dispose(); } // ── PTT gesture handlers ─────────────────────────────────────────── Future _pttPress() async { if (_pttDown) return; HapticFeedback.heavyImpact(); setState(() => _pttDown = true); if (_listening) { await widget.session.stopListening(); setState(() => _listening = false); } await widget.session.pttPress(); } Future _pttRelease() async { if (!_pttDown) return; HapticFeedback.lightImpact(); setState(() => _pttDown = false); await widget.session.pttRelease(); } Future _toggleListen() async { if (_pttDown) return; if (_listening) { await widget.session.stopListening(); setState(() => _listening = false); } else { await widget.session.startListening(); setState(() => _listening = true); } } Future _endCall() async { await widget.session.endCall(); if (mounted) Navigator.of(context).pop(); } // ── Build ────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0D0D1A), appBar: AppBar( backgroundColor: const Color(0xFF0D0D1A), title: const Text('SECURE CALL', style: TextStyle(color: Colors.white, letterSpacing: 2)), centerTitle: true, automaticallyImplyLeading: false, actions: [ IconButton( icon: const Icon(Icons.call_end, color: Colors.redAccent), tooltip: 'End call', onPressed: _endCall, ), ], ), body: SafeArea( child: Column( children: [ const SizedBox(height: 12), // ── Status indicator ──────────────────────────────────── StatusIndicator(state: _state), const SizedBox(height: 24), // ── Level meters ──────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _meterColumn('TX', _txLevel, const Color(0xFFFF6D00)), _meterColumn('RX', _rxLevel, const Color(0xFF00B0FF)), ], ), ), const SizedBox(height: 24), // ── Stats row ──────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _statChip('TX FRAMES', '$_txFrames', const Color(0xFFFF6D00)), _statChip('RX FRAMES', '$_rxFrames', const Color(0xFF00B0FF)), _statChip('RX ERRORS', '$_rxErrors', _rxErrors > 0 ? Colors.redAccent : Colors.white38), ], ), ), const Spacer(), // ── Operation hint ─────────────────────────────────────── Text( _pttDown ? 'TRANSMITTING — release to stop' : _listening ? 'RECEIVING — tap Listen to stop' : 'Hold PTT to speak', style: const TextStyle(color: Colors.white38, fontSize: 12, letterSpacing: 1), ), const SizedBox(height: 28), // ── PTT button ────────────────────────────────────────── GestureDetector( onTapDown: (_) => _pttPress(), onTapUp: (_) => _pttRelease(), onTapCancel: () => _pttRelease(), child: AnimatedContainer( duration: const Duration(milliseconds: 80), width: _pttDown ? 140 : 128, height: _pttDown ? 140 : 128, decoration: BoxDecoration( shape: BoxShape.circle, color: _pttDown ? const Color(0xFFFF6D00) : const Color(0xFF1A1A2E), border: Border.all( color: _pttDown ? const Color(0xFFFF6D00) : const Color(0xFF333355), width: 3, ), boxShadow: _pttDown ? [const BoxShadow( color: Color(0x80FF6D00), blurRadius: 32, spreadRadius: 8, )] : [], ), child: Center( child: Icon( Icons.mic, size: 52, color: _pttDown ? Colors.white : Colors.white38, ), ), ), ), const SizedBox(height: 8), const Text('PTT', style: TextStyle( color: Colors.white38, fontSize: 12, letterSpacing: 3)), const SizedBox(height: 28), // ── Listen / End call buttons ──────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _toggleListen, icon: Icon( _listening ? Icons.hearing_disabled : Icons.hearing, size: 18), label: Text(_listening ? 'STOP RX' : 'LISTEN', style: const TextStyle(letterSpacing: 1.5)), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF00B0FF), side: BorderSide( color: _listening ? const Color(0xFF00B0FF) : Colors.white24), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton.icon( onPressed: _endCall, icon: const Icon(Icons.call_end, size: 18), label: const Text('END CALL', style: TextStyle(letterSpacing: 1.5)), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), ), ], ), ), const SizedBox(height: 24), // ── Acoustic coupling reminder ─────────────────────────── Container( margin: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.white12), ), child: const Row( children: [ Icon(Icons.volume_up, color: Colors.white38, size: 16), SizedBox(width: 8), Expanded( child: Text( 'Keep phone in SPEAKERPHONE mode. ' 'Hold mic close to earpiece when receiving.', style: TextStyle( color: Colors.white38, fontSize: 11, height: 1.4), ), ), ], ), ), const SizedBox(height: 16), ], ), ), ); } // ── Helper widgets ───────────────────────────────────────────────── static Widget _meterColumn(String label, double level, Color color) => Column( children: [ Text(label, style: TextStyle( color: color, fontSize: 11, letterSpacing: 2)), const SizedBox(height: 6), LevelMeter(level: level, width: 28, height: 110), ], ); static Widget _statChip(String label, String value, Color color) => Column( children: [ Text(value, style: TextStyle( color: color, fontSize: 20, fontWeight: FontWeight.bold, fontFamily: 'monospace')), Text(label, style: const TextStyle(color: Colors.white38, fontSize: 10, letterSpacing: 1)), ], ); }