import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../models/channel.dart'; import '../services/auth_service.dart'; import '../services/ptt_service.dart'; import '../config/app_config.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; PttState _state = PttState.idle; String? _speaker; // Pulse animation for speaking/receiving states late final AnimationController _pulseCtrl; late final Animation _pulseAnim; // ── Wrist-flip detection (gyroscope) ──────────────────────────────── static const double _gyroThreshold = 9.0; // rad/s static const Duration _flipWindow = Duration(milliseconds: 900); static const Duration _flipCooldown = Duration(milliseconds: 380); bool _wristEnabled = false; bool _wristAvailable = false; DateTime? _firstFlipTime; DateTime? _lastFlipTime; StreamSubscription? _gyroSub; @override void initState() { super.initState(); _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), ), ), ); }); WakelockPlus.enable(); _connect(); _checkGyroscope(); } Future _checkGyroscope() async { try { final sub = gyroscopeEventStream().listen((_) {}); await sub.cancel(); if (mounted) setState(() => _wristAvailable = true); } catch (_) {} } void _toggleWrist() { setState(() => _wristEnabled = !_wristEnabled); if (_wristEnabled) { _gyroSub = gyroscopeEventStream().listen(_onGyroscope); } else { _gyroSub?.cancel(); _gyroSub = null; _firstFlipTime = null; _lastFlipTime = null; } } void _onGyroscope(GyroscopeEvent event) { final magnitude = sqrt( event.x * event.x + event.y * event.y + event.z * event.z, ); if (magnitude < _gyroThreshold) return; final now = DateTime.now(); if (_lastFlipTime != null && now.difference(_lastFlipTime!) < _flipCooldown) { return; } _lastFlipTime = now; if (_firstFlipTime == null || now.difference(_firstFlipTime!) > _flipWindow) { _firstFlipTime = now; } else { _firstFlipTime = null; _onPttTap(); } } Future _connect() async { final token = await AuthService().getToken(); if (!mounted) return; if (token == 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.connectToGroup( widget.channel.id, token, AppConfig.livekitUrl, ); } @override void dispose() { WakelockPlus.disable(); _gyroSub?.cancel(); _ptt.dispose(); _pulseCtrl.dispose(); super.dispose(); } Future _onPttTap() async { if (_state == PttState.connected) { HapticFeedback.heavyImpact(); await _ptt.startSpeaking(); } else if (_state == PttState.speaking) { HapticFeedback.mediumImpact(); await _ptt.stopSpeaking(); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Channel name Padding( padding: const EdgeInsets.symmetric(horizontal: 16), 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: 10), // 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: 10), // Bottom bar: back + wrist-flip toggle Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Back button 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.white70, size: 18, ), ), // Wrist-flip toggle (فقط اگه ژیروسکوپ دارد) if (_wristAvailable) ...[ const SizedBox(width: 16), AnimatedContainer( duration: const Duration(milliseconds: 200), width: 36, height: 36, decoration: BoxDecoration( color: _wristEnabled ? const Color(0xFF00C853).withValues(alpha: 0.2) : Colors.transparent, shape: BoxShape.circle, ), child: IconButton( onPressed: _toggleWrist, padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 36, minHeight: 36, ), icon: Icon( Icons.rotate_right, color: _wristEnabled ? const Color(0xFF00C853) : Colors.white38, size: 18, ), ), ), ], ], ), ], ), ), ); } } // ──────────────────────────────────────────── 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: 110, height: 110, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withValues(alpha: enabled ? 0.45 : 0.15), blurRadius: state == PttState.speaking ? 20 : 8, spreadRadius: state == PttState.speaking ? 4 : 2, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: Colors.white, size: 32), const SizedBox(height: 3), Text( label, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10, letterSpacing: 0.5, ), ), ], ), ); } }