import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:onufitness/routes/route_constant.dart'; import 'package:onufitness/services/agora/call_services.dart'; // Outgoing call screen - Shows when you initiate a call class OutgoingCallScreen extends StatelessWidget { final callService = AgoraCallService.instance; OutgoingCallScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Obx(() { // Auto navigate to call screen when call is accepted/connected if (callService.callState.value == CallState.connected) { WidgetsBinding.instance.addPostFrameCallback((_) { Get.offNamed(RouteConstant.activeCallScreen); }); } return SafeArea( child: Stack( children: [ if (callService.currentCallType.value == CallType.video) SizedBox( width: double.infinity, height: double.infinity, child: callService.isVideoPreviewStarted.value ? AgoraVideoView( controller: VideoViewController( rtcEngine: callService.rtcEngine!, canvas: const VideoCanvas(uid: 0), ), ) : Container( color: Colors.grey[900], child: Center( child: CircularProgressIndicator( color: Colors.white, ), ), ), ), // Gradient overlay for better text visibility Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.7), Colors.transparent, Colors.transparent, Colors.black.withValues(alpha: 0.8), ], stops: [0.0, 0.3, 0.7, 1.0], ), ), ), Column( children: [ // Top section with back button and encryption info Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Icon(Icons.lock, color: Colors.white, size: 16.sp), SizedBox(width: 4), Text( 'End-to-end encrypted', style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w300, ), ), ], ), ], ), ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Only show profile picture for voice calls if (callService.currentCallType.value == CallType.voice) CircleAvatar( radius: 80.r, backgroundColor: Colors.grey[800], child: callService.callerProfilePic.value.isNotEmpty ? ClipOval( child: Image.network( callService.callerProfilePic.value, fit: BoxFit.cover, filterQuality: FilterQuality.low, width: 160.r, height: 160.r, ), ) : Text( callService.callerName.value.isNotEmpty ? callService.callerName.value[0] .toUpperCase() : callService .remoteUserId .value .isNotEmpty ? callService.remoteUserId.value[0] .toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 42.sp, fontWeight: FontWeight.bold, ), ), ), if (callService.currentCallType.value == CallType.voice) SizedBox(height: 20), // Caller name Text( callService.callerName.value.isNotEmpty ? callService.callerName.value : callService.remoteUserId.value, style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w500, shadows: [ Shadow( offset: Offset(0, 1), blurRadius: 3, color: Colors.black.withValues(alpha: 0.8), ), ], ), ), SizedBox(height: 8), Text( callService.callState.value == CallState.calling ? 'Calling...' : callService.callState.value == CallState.idle ? 'Call ended' : 'Connecting...', style: TextStyle( color: Colors.white70, fontSize: 16.sp, shadows: [ Shadow( offset: Offset(0, 1), blurRadius: 3, color: Colors.black.withValues(alpha: 0.8), ), ], ), ), ], ), ), // Call type indicator Container( margin: EdgeInsets.only(bottom: 40), padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(20), border: Border.all( color: Colors.white.withValues(alpha: 0.2), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( callService.currentCallType.value == CallType.video ? Icons.videocam : Icons.call, color: Colors.white, size: 20, ), SizedBox(width: 8), Text( callService.currentCallType.value == CallType.video ? 'Video calling' : 'Voice calling', style: TextStyle(color: Colors.white), ), ], ), ), // Call controls Padding( padding: EdgeInsets.only(bottom: 60), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ callActionButton( icon: Icons.call_end, color: Colors.red, onTap: () async { await callService.endCall(); }, ), ], ), ), ], ), ], ), ); }), ); } Widget callActionButton({ required IconData icon, required Color color, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: 70, height: 70, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.4), blurRadius: 10, spreadRadius: 2, ), ], ), child: Icon(icon, color: Colors.white, size: 32), ), ); } } // Incoming call screen with animations - Shows when someone calls you class IncomingCallScreen extends StatefulWidget { const IncomingCallScreen({super.key}); @override State createState() => _IncomingCallScreenState(); } class _IncomingCallScreenState extends State with TickerProviderStateMixin { final callService = Get.find(); late AnimationController _pulseController; late AnimationController _slideController; late AnimationController _fadeController; late AnimationController _rippleController; late Animation _pulseAnimation; late Animation _slideAnimation; late Animation _fadeAnimation; late Animation _rippleAnimation; @override void initState() { super.initState(); // Pulse animation for profile picture _pulseController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(reverse: true); _pulseAnimation = Tween(begin: 1.0, end: 1.05).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); // Slide animation for content _slideController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, )..forward(); _slideAnimation = Tween( begin: const Offset(0, 0.3), end: Offset.zero, ).animate( CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), ); // Fade animation _fadeController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, )..forward(); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn)); // Ripple animation for buttons _rippleController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, )..repeat(); _rippleAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _rippleController, curve: Curves.easeOut), ); } @override void dispose() { _pulseController.dispose(); _slideController.dispose(); _fadeController.dispose(); _rippleController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Obx(() { final hasValidData = callService.callerName.value.isNotEmpty && callService.channelName.value.isNotEmpty; if (!hasValidData) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 15.h), Text('Connecting...', style: TextStyle(color: Colors.white)), ], ), ); } if (callService.callState.value == CallState.connected) { WidgetsBinding.instance.addPostFrameCallback((_) { Get.offNamed(RouteConstant.activeCallScreen); }); } return SafeArea( child: Stack( children: [ if (callService.currentCallType.value == CallType.video) SizedBox( width: double.infinity, height: double.infinity, child: callService.isVideoPreviewStarted.value ? AgoraVideoView( controller: VideoViewController( rtcEngine: callService.rtcEngine!, canvas: const VideoCanvas(uid: 0), ), ) : Container( color: Colors.grey[900], child: Center( child: CircularProgressIndicator( color: Colors.white, ), ), ), ), Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.7), Colors.transparent, Colors.transparent, Colors.black.withValues(alpha: 0.9), ], stops: [0.0, 0.3, 0.6, 1.0], ), ), ), FadeTransition( opacity: _fadeAnimation, child: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Icon( Icons.lock, color: Colors.white, size: 16.sp, ), SizedBox(width: 4), Text( 'End-to-end encrypted', style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w300, ), ), ], ), ], ), ), Expanded( child: SlideTransition( position: _slideAnimation, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Animated call type indicator TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 600), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Container( padding: EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( color: Colors.black.withValues( alpha: 0.6, ), borderRadius: BorderRadius.circular(20), border: Border.all( color: Colors.white.withValues( alpha: 0.2, ), width: 1, ), ), child: Text( 'Incoming ${callService.currentCallType.value.name} call', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w400, ), ), ), ); }, ), SizedBox(height: 40), // Animated profile picture with pulse and ripples if (callService.currentCallType.value == CallType.voice) Stack( alignment: Alignment.center, children: [ // Outer ripple effect AnimatedBuilder( animation: _rippleAnimation, builder: (context, child) { return Container( width: 200 + (_rippleAnimation.value * 40), height: 200 + (_rippleAnimation.value * 40), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.white.withValues( alpha: 0.3 * (1 - _rippleAnimation.value), ), width: 2, ), ), ); }, ), // Inner ripple effect AnimatedBuilder( animation: _rippleAnimation, builder: (context, child) { return Container( width: 180 + (_rippleAnimation.value * 30), height: 180 + (_rippleAnimation.value * 30), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.white.withValues( alpha: 0.5 * (1 - _rippleAnimation.value), ), width: 2, ), ), ); }, ), // Pulsing profile picture ScaleTransition( scale: _pulseAnimation, child: CircleAvatar( radius: 80, backgroundColor: Colors.grey[800], backgroundImage: callService .callerProfilePic .value .isNotEmpty ? NetworkImage( callService .callerProfilePic .value, ) : null, child: callService .callerProfilePic .value .isEmpty ? Text( callService .callerName .value .isNotEmpty ? callService .callerName .value[0] .toUpperCase() : callService .remoteUserId .value .isNotEmpty ? callService .remoteUserId .value[0] .toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold, ), ) : null, ), ), ], ), SizedBox(height: 20), // Caller name with fade-in Text( callService.callerName.value.isNotEmpty ? callService.callerName.value : callService.remoteUserId.value, style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w400, ), ), SizedBox(height: 30), // Call type indicator with icon animation TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 800), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Container( padding: EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), decoration: BoxDecoration( color: Colors.grey[900], borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( callService.currentCallType.value == CallType.video ? Icons.videocam : Icons.call, color: Colors.white, size: 20, ), SizedBox(width: 8), Text( callService.currentCallType.value == CallType.video ? 'Video Call' : 'Voice Call', style: TextStyle(color: Colors.white), ), ], ), ), ); }, ), ], ), ), ), // Animated call control buttons Padding( padding: EdgeInsets.only(bottom: 60), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Reject button with ripple TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 500), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: _buildAnimatedButton( icon: Icons.call_end, color: Colors.red, onTap: () async { await callService.rejectCall(); }, ), ); }, ), // Accept button with ripple and bounce TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 700), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: _buildAnimatedButton( icon: Icons.call, color: Colors.green, onTap: () { callService.acceptCall( callType: callService.currentCallType.value, ); }, isPrimary: true, ), ); }, ), ], ), ), ], ), ), ], ), ); }), ); } Widget _buildAnimatedButton({ required IconData icon, required Color color, required VoidCallback onTap, bool isPrimary = false, }) { return Stack( alignment: Alignment.center, children: [ // Pulsing ring for accept button if (isPrimary) AnimatedBuilder( animation: _rippleAnimation, builder: (context, child) { return Container( width: 90 + (_rippleAnimation.value * 20), height: 90 + (_rippleAnimation.value * 20), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: color.withValues( alpha: 0.4 * (1 - _rippleAnimation.value), ), width: 3, ), ), ); }, ), // Button GestureDetector( onTap: onTap, child: AnimatedContainer( duration: Duration(milliseconds: 200), width: 70, height: 70, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.5), blurRadius: 20, spreadRadius: 5, ), ], ), child: Icon(icon, color: Colors.white, size: 32), ), ), ], ); } } // Active call screen - Shows when call is connected class CallScreen extends StatefulWidget { const CallScreen({super.key}); @override State createState() => _CallScreenState(); } class _CallScreenState extends State { final callService = Get.find(); final RxBool isVideoSwapped = false.obs; // Track if videos are swapped @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return PopScope( onPopInvokedWithResult: (didPop, result) {}, child: Scaffold( backgroundColor: Colors.black, body: SafeArea( child: Obx(() { final hasValidData = callService.callerName.value.isNotEmpty && callService.channelName.value.isNotEmpty; if (!hasValidData) { // Show loading while waiting for data return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 15.h), Text('Connecting...'), ], ), ); } // ✅ Show connecting state while call is being established if (callService.callState.value == CallState.ringing) { return _buildConnectingUI(); } return callService.currentCallType.value == CallType.video ? _buildVideoCallUI() : _buildVoiceCallUI(); }), ), ), ); } // ✅ NEW: Show smooth connecting UI instead of IncomingCallScreen buttons Widget _buildConnectingUI() { return Stack( children: [ // Show video preview if available if (callService.currentCallType.value == CallType.video) SizedBox( width: double.infinity, height: double.infinity, child: callService.isVideoPreviewStarted.value ? AgoraVideoView( controller: VideoViewController( rtcEngine: callService.rtcEngine!, canvas: const VideoCanvas(uid: 0), ), ) : Container( color: Colors.grey[900], child: Center( child: CircularProgressIndicator(color: Colors.white), ), ), ), // Gradient overlay Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.7), Colors.transparent, Colors.transparent, Colors.black.withValues(alpha: 0.8), ], stops: [0.0, 0.3, 0.7, 1.0], ), ), ), // Content Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Profile picture for voice calls if (callService.currentCallType.value == CallType.voice) CircleAvatar( radius: 80, backgroundColor: Colors.grey[800], backgroundImage: callService.callerProfilePic.value.isNotEmpty ? NetworkImage(callService.callerProfilePic.value) : null, child: callService.callerProfilePic.value.isEmpty ? Text( callService.callerName.value.isNotEmpty ? callService.callerName.value[0].toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold, ), ) : null, ), SizedBox(height: 24), // Caller name Text( callService.callerName.value, style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w500, ), ), SizedBox(height: 16), // Connecting indicator with animation Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white70), ), ), SizedBox(width: 12), Text( 'Connecting...', style: TextStyle(color: Colors.white70, fontSize: 16), ), ], ), ], ), ], ); } Widget _buildVideoCallUI() { return Stack( children: [ // Main video view (full screen) - Shows remote by default, local when swapped SizedBox( width: double.infinity, height: double.infinity, child: Obx(() { // When swapped, show local video in main view if (isVideoSwapped.value) { return callService.isLocalVideoEnabled.value ? AgoraVideoView( controller: VideoViewController( rtcEngine: callService.rtcEngine!, canvas: const VideoCanvas(uid: 0), ), ) : Container( color: Colors.grey[900], child: Center( child: Icon( Icons.videocam_off, color: Colors.white, size: 64, ), ), ); } // Default: Show remote video in main view // Show profile picture if remote user hasn't joined OR if remote video is disabled if (callService.integerRemoteUserUid.value == 0 || !callService.isRemoteVideoEnabled.value) { return Container( color: Colors.grey[900], child: Center( child: CircleAvatar( radius: 80.r, backgroundColor: Colors.grey[800], backgroundImage: callService.callerProfilePic.value.isNotEmpty ? NetworkImage(callService.callerProfilePic.value) : null, child: callService.callerProfilePic.value.isEmpty ? Text( callService.callerName.value.isNotEmpty ? callService.callerName.value[0] .toUpperCase() : callService.remoteUserId.value.isNotEmpty ? callService.remoteUserId.value[0] .toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 42.sp, fontWeight: FontWeight.bold, ), ) : null, ), ), ); } // Show remote video when available and enabled return AgoraVideoView( controller: VideoViewController.remote( rtcEngine: callService.rtcEngine!, canvas: VideoCanvas( uid: callService.integerRemoteUserUid.value, ), connection: RtcConnection( channelId: callService.channelName.value, ), ), ); }), ), // Top overlay with caller info and call duration Positioned( top: 0, left: 0, right: 0, child: Container( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.8), Colors.transparent, ], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( children: [ Icon(Icons.lock, color: Colors.white, size: 16.sp), SizedBox(width: 4), Text( 'End to End Encrypted', style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w300, ), ), ], ), SizedBox(height: 16), Text( callService.callerName.value.isNotEmpty ? callService.callerName.value : callService.remoteUserId.value, style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.w500, ), ), SizedBox(height: 4), Obx( () => Text( _formatDuration(callService.callDuration.value), style: TextStyle(color: Colors.white70, fontSize: 14.sp), ), ), ], ), ), ), // Thumbnail video view (top right corner) - Shows local by default, remote when swapped Positioned( top: 80.h, right: 20.w, child: GestureDetector( onTap: () { // Toggle the swap state isVideoSwapped.value = !isVideoSwapped.value; }, child: Container( width: 120.w, height: 160.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.white.withValues(alpha: 0.3), width: 1, ), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Obx(() { // When swapped, show remote video in thumbnail if (isVideoSwapped.value) { if (callService.integerRemoteUserUid.value == 0 || !callService.isRemoteVideoEnabled.value) { return Container( color: Colors.black87, child: Center( child: CircleAvatar( radius: 30.r, backgroundColor: Colors.grey[800], backgroundImage: callService.callerProfilePic.value.isNotEmpty ? NetworkImage( callService.callerProfilePic.value, ) : null, child: callService.callerProfilePic.value.isEmpty ? Text( callService.callerName.value.isNotEmpty ? callService.callerName.value[0] .toUpperCase() : callService .remoteUserId .value .isNotEmpty ? callService.remoteUserId.value[0] .toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 20.sp, fontWeight: FontWeight.bold, ), ) : null, ), ), ); } return AgoraVideoView( controller: VideoViewController.remote( rtcEngine: callService.rtcEngine!, canvas: VideoCanvas( uid: callService.integerRemoteUserUid.value, ), connection: RtcConnection( channelId: callService.channelName.value, ), ), ); } // Default: Show local video in thumbnail return callService.isLocalVideoEnabled.value ? AgoraVideoView( controller: VideoViewController( rtcEngine: callService.rtcEngine!, canvas: const VideoCanvas(uid: 0), ), ) : Container( color: Colors.black87, child: Center( child: Icon( Icons.videocam_off, color: Colors.white, size: 32, ), ), ); }), ), ), ), ), // Bottom controls Positioned( bottom: 0, left: 0, right: 0, child: Container( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 40.w), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withValues(alpha: 0.8), Colors.transparent, ], ), ), child: _buildCallControls(), ), ), ], ); } Widget _buildVoiceCallUI() { return Column( children: [ // Top section Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.lock, color: Colors.white, size: 16.sp), SizedBox(width: 4), Text( 'End to End Encrypted', style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w300, ), ), ], ), ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircleAvatar( radius: 80, backgroundColor: Colors.grey[800], backgroundImage: callService.callerProfilePic.value.isNotEmpty ? NetworkImage(callService.callerProfilePic.value) : null, child: callService.callerProfilePic.value.isEmpty ? Text( callService.callerName.value.isNotEmpty ? callService.callerName.value[0].toUpperCase() : callService.remoteUserId.value.isNotEmpty ? callService.remoteUserId.value[0].toUpperCase() : 'U', style: TextStyle( color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold, ), ) : null, ), SizedBox(height: 20), Text( callService.callerName.value.isNotEmpty ? callService.callerName.value : callService.remoteUserId.value, style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w400, ), ), SizedBox(height: 10), Obx( () => Text( _formatDuration(callService.callDuration.value), style: TextStyle(color: Colors.grey[400], fontSize: 16), ), ), ], ), ), Padding( padding: EdgeInsets.only(bottom: 60), child: _buildCallControls(), ), ], ); } Widget _buildCallControls() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Mute/Unmute button Obx( () => _buildControlButton( icon: callService.isLocalAudioEnabled.value ? Icons.mic : Icons.mic_off, color: callService.isLocalAudioEnabled.value ? Colors.grey[700]! : Colors.red, onTap: callService.toggleLocalAudio, ), ), SizedBox(width: 5.w), // Speaker button (for both voice and video calls) Obx( () => _buildControlButton( icon: callService.isSpeakerEnabled.value ? Icons.volume_up : Icons.volume_down, color: callService.isSpeakerEnabled.value ? Colors.blue : Colors.grey[700]!, onTap: callService.toggleSpeaker, ), ), // Video controls (only for video calls) if (callService.currentCallType.value == CallType.video) ...[ SizedBox(width: 5.w), Obx( () => _buildControlButton( icon: callService.isLocalVideoEnabled.value ? Icons.videocam : Icons.videocam_off, color: callService.isLocalVideoEnabled.value ? Colors.grey[700]! : Colors.red, onTap: callService.toggleLocalVideo, ), ), SizedBox(width: 5.w), _buildControlButton( icon: Icons.flip_camera_ios, color: Colors.grey[700]!, onTap: callService.switchCamera, ), ], SizedBox(width: 5.w), // End call button _buildControlButton( icon: Icons.call_end, color: Colors.red, onTap: () async { await callService.endCall(); }, ), ], ); } Widget _buildControlButton({ required IconData icon, required Color color, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( width: 60, height: 60, decoration: BoxDecoration(color: color, shape: BoxShape.circle), child: Icon(icon, color: Colors.white, size: 24), ), ); } String _formatDuration(int seconds) { int minutes = seconds ~/ 60; int remainingSeconds = seconds % 60; return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; } }