// ignore_for_file: use_build_context_synchronously // lib/features/call/call_screen.dart // // CallScreen — user memanggil Guardian via Agora // IncomingCallScreen — Guardian/User menerima panggilan masuk // // Keduanya pakai CallService yang sudah ada (agora_rtc_engine). import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../app/injection_container.dart'; import '../../core/services/call_service.dart'; import '../../core/services/haptic_service.dart'; import '../../core/services/tts_service.dart'; // ─── Colours ───────────────────────────────────────────────────────────────── const _kBlue = Color(0xFF1A56DB); const _kGreen = Color(0xFF16A34A); const _kRed = Color(0xFFDC2626); const _kMuted = Color(0xFF64748B); const _kBg = Color(0xFF0F172A); // dark bg untuk call screen // ─── CallScreen ─────────────────────────────────────────────────────────────── class CallScreen extends StatefulWidget { const CallScreen({super.key}); @override State createState() => _CallScreenState(); } class _CallScreenState extends State with SingleTickerProviderStateMixin { static const _channelName = 'walkguide-call'; _CallPhase _phase = _CallPhase.calling; bool _muted = false; bool _speakerOn = true; int _secondsElapsed = 0; Timer? _timer; // animasi pulse saat ringing late AnimationController _pulseCtrl; late Animation _pulseScale; @override void initState() { super.initState(); _pulseCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(reverse: true); _pulseScale = Tween(begin: 0.95, end: 1.08) .animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut)); sl().speak('Memanggil Guardian.'); _startCall(); } Future _startCall() async { final joined = await sl().joinChannel(channelName: _channelName); if (!mounted) return; if (joined) { setState(() => _phase = _CallPhase.connected); sl().speak('Terhubung dengan Guardian.'); _pulseCtrl.stop(); _startTimer(); } else { setState(() => _phase = _CallPhase.failed); sl() .speak('Panggilan gagal. Pastikan Agora sudah dikonfigurasi.'); } } void _startTimer() { _timer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() => _secondsElapsed++); }); } Future _endCall() async { _timer?.cancel(); await sl().leave(); sl().speak('Panggilan diakhiri.'); if (mounted) context.go('/user/walkguide'); } Future _toggleMute() async { setState(() => _muted = !_muted); // Agora engine mute via CallService jika ada — di sini cukup state lokal // sl().muteLocalAudio(_muted); } void _toggleSpeaker() { setState(() => _speakerOn = !_speakerOn); } String get _timerLabel { final m = (_secondsElapsed ~/ 60).toString().padLeft(2, '0'); final s = (_secondsElapsed % 60).toString().padLeft(2, '0'); return '$m:$s'; } @override void dispose() { _timer?.cancel(); _pulseCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _kBg, body: SafeArea( child: Column( children: [ // ── top bar ────────────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ IconButton( onPressed: () => context.go('/user/walkguide'), icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white54), ), const Expanded( child: Text('Panggilan', textAlign: TextAlign.center, style: TextStyle( color: Colors.white70, fontWeight: FontWeight.w600)), ), const SizedBox(width: 48), // balance ], ), ), const Spacer(), // ── avatar + name ──────────────────────────────────────────── AnimatedBuilder( animation: _pulseCtrl, builder: (_, child) => Transform.scale( scale: _phase == _CallPhase.calling ? _pulseScale.value : 1.0, child: child, ), child: Container( width: 120, height: 120, decoration: BoxDecoration( shape: BoxShape.circle, color: _kBlue.withValues(alpha: 0.2), border: Border.all(color: _kBlue, width: 3), ), child: const Icon(Icons.shield_outlined, color: Colors.white, size: 56), ), ), const SizedBox(height: 20), const Text('Guardian', style: TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.w800)), const SizedBox(height: 8), _PhaseLabel(phase: _phase, timerLabel: _timerLabel), const Spacer(), // ── controls ───────────────────────────────────────────────── if (_phase == _CallPhase.connected) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ControlButton( icon: _muted ? Icons.mic_off : Icons.mic, label: _muted ? 'Unmute' : 'Mute', onTap: _toggleMute, active: _muted, ), _ControlButton( icon: _speakerOn ? Icons.volume_up : Icons.volume_off, label: _speakerOn ? 'Speaker' : 'Earpiece', onTap: _toggleSpeaker, active: _speakerOn, ), ], ), const SizedBox(height: 28), ], if (_phase == _CallPhase.failed) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( 'Panggilan gagal.\nPastikan Agora App ID sudah diisi di app_constants.dart dan server backend aktif.', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white54, height: 1.5), ), ), const SizedBox(height: 24), ], // ── end call button ─────────────────────────────────────────── _EndCallButton(onTap: _endCall), const SizedBox(height: 48), ], ), ), ); } } // ─── IncomingCallScreen ─────────────────────────────────────────────────────── class IncomingCallScreen extends StatefulWidget { /// callerName bisa diisi dari FCM payload via extra go_router params. /// Default 'Guardian' jika tidak ada. final String callerName; const IncomingCallScreen({super.key, this.callerName = 'Guardian'}); @override State createState() => _IncomingCallScreenState(); } class _IncomingCallScreenState extends State { static const _autoAnswerSeconds = 30; int _countdown = _autoAnswerSeconds; Timer? _autoTimer; bool _responding = false; @override void initState() { super.initState(); sl().callIncoming(); sl().speak('Panggilan masuk dari ${widget.callerName}.'); // auto-answer countdown _autoTimer = Timer.periodic(const Duration(seconds: 1), (t) { if (!mounted) { t.cancel(); return; } setState(() => _countdown--); if (_countdown <= 0) { t.cancel(); _accept(); } }); } @override void dispose() { _autoTimer?.cancel(); super.dispose(); } Future _accept() async { if (_responding) return; setState(() => _responding = true); _autoTimer?.cancel(); sl().speak('Menerima panggilan.'); // Gabung ke channel yang sama (nama channel dari FCM payload — sementara hardcode) await sl().joinChannel(channelName: 'walkguide-call'); if (mounted) context.go('/user/call'); } Future _decline() async { if (_responding) return; setState(() => _responding = true); _autoTimer?.cancel(); sl().speak('Panggilan ditolak.'); await sl().leave(); if (mounted) context.go('/user/walkguide'); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _kBg, body: SafeArea( child: Column( children: [ const Spacer(), // ── caller info ─────────────────────────────────────────────── const Icon(Icons.call_received, color: _kGreen, size: 48), const SizedBox(height: 16), const Text('Panggilan Masuk', style: TextStyle(color: Colors.white54, fontSize: 14)), const SizedBox(height: 8), Text(widget.callerName, style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w800)), const SizedBox(height: 12), // auto-answer countdown Text( 'Auto-answer dalam $_countdown detik', style: const TextStyle(color: Colors.white38, fontSize: 13), ), const Spacer(), // ── accept / decline ────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 48), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Decline _RoundCallButton( icon: Icons.call_end, color: _kRed, label: 'Tolak', onTap: _responding ? null : _decline, ), // Accept _RoundCallButton( icon: Icons.call, color: _kGreen, label: 'Terima', onTap: _responding ? null : _accept, ), ], ), ), const SizedBox(height: 56), ], ), ), ); } } // ─── Sub-widgets ────────────────────────────────────────────────────────────── enum _CallPhase { calling, connected, failed } class _PhaseLabel extends StatelessWidget { final _CallPhase phase; final String timerLabel; const _PhaseLabel({required this.phase, required this.timerLabel}); @override Widget build(BuildContext context) { switch (phase) { case _CallPhase.calling: return const Text('Memanggil…', style: TextStyle(color: _kMuted, fontSize: 16)); case _CallPhase.connected: return Text(timerLabel, style: const TextStyle( color: _kGreen, fontSize: 22, fontWeight: FontWeight.w700)); case _CallPhase.failed: return const Text('Panggilan gagal', style: TextStyle(color: _kRed, fontSize: 16)); } } } class _ControlButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; final bool active; const _ControlButton( {required this.icon, required this.label, required this.onTap, this.active = false}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Column( children: [ Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, color: active ? Colors.white.withValues(alpha: 0.25) : Colors.white.withValues(alpha: 0.1), ), child: Icon(icon, color: Colors.white, size: 28), ), const SizedBox(height: 6), Text(label, style: const TextStyle(color: Colors.white54, fontSize: 12)), ], ), ); } } class _EndCallButton extends StatelessWidget { final VoidCallback onTap; const _EndCallButton({required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Column( children: [ Container( width: 72, height: 72, decoration: const BoxDecoration( shape: BoxShape.circle, color: _kRed, ), child: const Icon(Icons.call_end, color: Colors.white, size: 32), ), const SizedBox(height: 6), const Text('Akhiri', style: TextStyle(color: Colors.white54, fontSize: 12)), ], ), ); } } class _RoundCallButton extends StatelessWidget { final IconData icon; final Color color; final String label; final VoidCallback? onTap; const _RoundCallButton( {required this.icon, required this.color, required this.label, this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Opacity( opacity: onTap == null ? 0.4 : 1.0, child: Column( children: [ Container( width: 72, height: 72, decoration: BoxDecoration(shape: BoxShape.circle, color: color), child: Icon(icon, color: Colors.white, size: 32), ), const SizedBox(height: 8), Text(label, style: const TextStyle(color: Colors.white70, fontSize: 13)), ], ), ), ); } }