import 'package:flutter/material.dart'; import '../models/channel.dart'; import '../services/api_service.dart'; import '../services/auth_service.dart'; import '../services/ptt_service.dart'; class ChannelScreen extends StatefulWidget { final Channel channel; const ChannelScreen({super.key, required this.channel}); @override State createState() => _ChannelScreenState(); } class _ChannelScreenState extends State with SingleTickerProviderStateMixin { late final PttService _ptt; late final ApiService _api; PttState _state = PttState.idle; String? _speaker; // Pulse animation for speaking/receiving states late final AnimationController _pulseCtrl; late final Animation _pulseAnim; @override void initState() { super.initState(); final auth = AuthService(); _api = ApiService(auth); _ptt = PttService(); _pulseCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); _pulseAnim = Tween(begin: 1.0, end: 1.12).animate( CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut), ); _ptt.stateStream.listen((s) { if (!mounted) return; setState(() => _state = s); if (s == PttState.speaking || s == PttState.receiving) { _pulseCtrl.repeat(reverse: true); } else { _pulseCtrl.stop(); _pulseCtrl.reset(); } }); _ptt.speakerStream.listen((name) { if (!mounted) return; setState(() => _speaker = name); }); _ptt.errorStream.listen((err) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(err, style: const TextStyle(fontSize: 11), textAlign: TextAlign.center), duration: const Duration(seconds: 2), backgroundColor: const Color(0xFF333333), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 40), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), ); }); _connect(); } Future _connect() async { final creds = await _api.getLivekitToken(widget.channel.id); if (!mounted) return; if (creds == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('دریافت توکن ناموفق بود', style: TextStyle(fontSize: 11), textAlign: TextAlign.center), duration: const Duration(seconds: 3), backgroundColor: const Color(0xFF333333), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 40), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), ); return; } await _ptt.connect(creds.url, creds.token); } @override void dispose() { _ptt.dispose(); _pulseCtrl.dispose(); super.dispose(); } Future _onPttTap() async { if (_state == PttState.connected) { await _ptt.startSpeaking(); } else if (_state == PttState.speaking) { await _ptt.stopSpeaking(); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: SafeArea( child: Stack( children: [ // Back button — top-left Positioned( top: 2, left: 2, child: IconButton( onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 36, minHeight: 36), icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white38, size: 16), ), ), // Main content Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Channel name Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Text( widget.channel.name, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ), const SizedBox(height: 20), // PTT Button Center( child: AnimatedBuilder( animation: _pulseAnim, builder: (_, child) => Transform.scale(scale: _pulseAnim.value, child: child), child: GestureDetector( onTap: _onPttTap, child: _PttButton(state: _state, speaker: _speaker), ), ), ), const SizedBox(height: 14), // Status text _StatusText(state: _state, speaker: _speaker), ], ), ], ), ), ); } } // ──────────────────────────────────────────── class _PttButton extends StatelessWidget { final PttState state; final String? speaker; const _PttButton({required this.state, this.speaker}); @override Widget build(BuildContext context) { final (color, icon, label) = switch (state) { PttState.speaking => ( const Color(0xFFFF1744), Icons.mic, 'TALKING' ), PttState.receiving => ( const Color(0xFF2979FF), Icons.volume_up, speaker ?? '...', ), PttState.connected => ( const Color(0xFF00C853), Icons.settings_input_antenna, 'PUSH', ), PttState.idle => ( const Color(0xFF424242), Icons.wifi_off, '...', ), }; final bool enabled = state == PttState.connected || state == PttState.speaking; return AnimatedContainer( duration: const Duration(milliseconds: 250), width: 130, height: 130, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withValues(alpha: enabled ? 0.45 : 0.15), blurRadius: state == PttState.speaking ? 24 : 10, spreadRadius: state == PttState.speaking ? 6 : 2, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: Colors.white, size: 38), const SizedBox(height: 4), Text( label, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 0.5, ), ), ], ), ); } } // ──────────────────────────────────────────── class _StatusText extends StatelessWidget { final PttState state; final String? speaker; const _StatusText({required this.state, this.speaker}); @override Widget build(BuildContext context) { final (text, color) = switch (state) { PttState.speaking => ('برای قطع کلیک کنید', const Color(0xFFFF1744)), PttState.receiving => ( '${speaker ?? '...'} در حال صحبت', const Color(0xFF2979FF) ), PttState.connected => ('برای صحبت کلیک کنید', Colors.white38), PttState.idle => ('در حال اتصال...', Colors.white24), }; return Text( text, style: TextStyle(color: color, fontSize: 10), textAlign: TextAlign.center, ); } }