You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
6.4 KiB
Dart
217 lines
6.4 KiB
Dart
|
1 month ago
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:provider/provider.dart';
|
||
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
||
|
|
import 'package:test_sa/extensions/int_extensions.dart';
|
||
|
|
import 'package:test_sa/extensions/string_extensions.dart';
|
||
|
|
import 'package:test_sa/extensions/text_extensions.dart';
|
||
|
|
import 'package:test_sa/extensions/widget_extensions.dart';
|
||
|
|
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||
|
|
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
||
|
|
import '../chat_provider.dart';
|
||
|
|
import '../model/call_session.dart';
|
||
|
|
import 'audio_call_page.dart';
|
||
|
|
import 'video_call_page.dart';
|
||
|
|
|
||
|
|
/// Native-style full-screen incoming call dialog matching audio call page design
|
||
|
|
class IncomingCallDialog extends StatefulWidget {
|
||
|
|
final CallSession call;
|
||
|
|
|
||
|
|
const IncomingCallDialog({
|
||
|
|
Key? key,
|
||
|
|
required this.call,
|
||
|
|
}) : super(key: key);
|
||
|
|
|
||
|
|
static void show(BuildContext context, CallSession call) {
|
||
|
|
Navigator.of(context).push(
|
||
|
|
MaterialPageRoute(
|
||
|
|
fullscreenDialog: true,
|
||
|
|
builder: (context) => IncomingCallDialog(call: call),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<IncomingCallDialog> createState() => _IncomingCallDialogState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _IncomingCallDialogState extends State<IncomingCallDialog> {
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
// Listen for call status changes
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
final chatProvider = context.read<ChatProvider>();
|
||
|
|
chatProvider.addListener(_checkCallStatus);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _checkCallStatus() {
|
||
|
|
if (!mounted) return; // Early exit if widget is unmounted
|
||
|
|
|
||
|
|
final chatProvider = context.read<ChatProvider>();
|
||
|
|
// If call status is idle (call cancelled/ended), automatically dismiss dialog
|
||
|
|
if (chatProvider.callStatus == CallStatus.idle) {
|
||
|
|
// Remove listener before popping
|
||
|
|
chatProvider.removeListener(_checkCallStatus);
|
||
|
|
Navigator.of(context).pop();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
// Clean up listener
|
||
|
|
try {
|
||
|
|
context.read<ChatProvider>().removeListener(_checkCallStatus);
|
||
|
|
} catch (e) {
|
||
|
|
// Provider might already be disposed
|
||
|
|
}
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final chatProvider = context.read<ChatProvider>();
|
||
|
|
|
||
|
|
return PopScope(
|
||
|
|
canPop: false, // Prevent dismissing by back button
|
||
|
|
child: Scaffold(
|
||
|
|
backgroundColor: AppColor.backgroundTabBarDark,
|
||
|
|
body: Column(
|
||
|
|
children: [
|
||
|
|
const Spacer(flex: 2),
|
||
|
|
_buildContactInfo(context, widget.call),
|
||
|
|
8.height,
|
||
|
|
_buildIncomingCallStatus(context),
|
||
|
|
const Spacer(flex: 4),
|
||
|
|
_buildActions(context, chatProvider, widget.call),
|
||
|
|
32.height,
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildContactInfo(BuildContext context, CallSession session) {
|
||
|
|
return Column(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.all(14),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
color: AppColor.whiteF8d,
|
||
|
|
border: Border.all(color: AppColor.white10.withOpacity(0.2), width: 1),
|
||
|
|
),
|
||
|
|
child: ClipOval(
|
||
|
|
child: session.peerAvatar != null
|
||
|
|
? CachedNetworkImage(
|
||
|
|
imageUrl: session.peerAvatar!,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
placeholder: (context, url) => Center(
|
||
|
|
child: CircularProgressIndicator(
|
||
|
|
color: Colors.white.withOpacity(0.5),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
errorWidget: (context, url, error) => 'call_user_avatar'.toSvgAsset(height: 48, width: 48),
|
||
|
|
)
|
||
|
|
: 'call_user_avatar'.toSvgAsset(height: 48, width: 48),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
24.height,
|
||
|
|
Text(
|
||
|
|
session.peerName,
|
||
|
|
style: AppTextStyles.heading2.copyWith(
|
||
|
|
color: Colors.white,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
),
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildIncomingCallStatus(BuildContext context) {
|
||
|
|
return Column(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.call.type == CallType.video ? 'Atoms Video Call' : 'Atoms Audio Call',
|
||
|
|
style: AppTextStyles.heading5.copyWith(
|
||
|
|
color: AppColor.neutral100,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
8.height,
|
||
|
|
Text(
|
||
|
|
'Incoming call...',
|
||
|
|
style: AppTextStyles.heading5.copyWith(
|
||
|
|
color: AppColor.neutral100,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildActions(BuildContext context, ChatProvider chatProvider, CallSession session) {
|
||
|
|
return Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
|
|
children: [
|
||
|
|
// Decline button
|
||
|
|
_buildActionButton(
|
||
|
|
icon: 'end_call',
|
||
|
|
label: 'Decline',
|
||
|
|
onPressed: () async {
|
||
|
|
Navigator.of(context).pop();
|
||
|
|
await chatProvider.declineCall('declined');
|
||
|
|
},
|
||
|
|
),
|
||
|
|
|
||
|
|
// Accept button
|
||
|
|
_buildActionButton(
|
||
|
|
icon: 'calling_icon',
|
||
|
|
label: 'Accept',
|
||
|
|
onPressed: () async {
|
||
|
|
// Just dismiss the dialog - acceptCall() will handle navigation
|
||
|
|
Navigator.of(context).pop();
|
||
|
|
await chatProvider.acceptCall();
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildActionButton({
|
||
|
|
required String icon,
|
||
|
|
required String label,
|
||
|
|
required VoidCallback onPressed,
|
||
|
|
}) {
|
||
|
|
return Column(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.all(18),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: icon == 'end_call' ? AppColor.red30 : Colors.green,
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.3),
|
||
|
|
blurRadius: 8,
|
||
|
|
offset: const Offset(0, 4),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
alignment: Alignment.center,
|
||
|
|
child: icon.toSvgAsset(height: 32, width: 32, color: AppColor.white10),
|
||
|
|
).onPress(onPressed),
|
||
|
|
8.height,
|
||
|
|
Text(
|
||
|
|
label,
|
||
|
|
style: AppTextStyles.heading5.copyWith(
|
||
|
|
color: AppColor.neutral100,
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|