2026-01-13 11:36:24 +05:30

1368 lines
52 KiB
Dart

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<IncomingCallScreen> createState() => _IncomingCallScreenState();
}
class _IncomingCallScreenState extends State<IncomingCallScreen>
with TickerProviderStateMixin {
final callService = Get.find<AgoraCallService>();
late AnimationController _pulseController;
late AnimationController _slideController;
late AnimationController _fadeController;
late AnimationController _rippleController;
late Animation<double> _pulseAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _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<double>(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<Offset>(
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<double>(
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<double>(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<double>(
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<double>(
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<double>(
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<double>(
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<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> {
final callService = Get.find<AgoraCallService>();
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<Color>(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')}';
}
}