Chat Fix & Web RTC Methods
parent
2b35c3d8d6
commit
74e01e60fb
Binary file not shown.
Binary file not shown.
@ -0,0 +1,61 @@
|
|||||||
|
// To parse this JSON data, do
|
||||||
|
//
|
||||||
|
// final remoteIceCandidatePayLoad = remoteIceCandidatePayLoadFromJson(jsonString);
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class RemoteIceCandidatePayLoad {
|
||||||
|
RemoteIceCandidatePayLoad({
|
||||||
|
this.target,
|
||||||
|
this.candidate,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? target;
|
||||||
|
Candidate? candidate;
|
||||||
|
|
||||||
|
factory RemoteIceCandidatePayLoad.fromRawJson(String str) => RemoteIceCandidatePayLoad.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory RemoteIceCandidatePayLoad.fromJson(Map<String, dynamic> json) => RemoteIceCandidatePayLoad(
|
||||||
|
target: json["target"],
|
||||||
|
candidate: json["candidate"] == null ? null : Candidate.fromJson(json["candidate"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"target": target,
|
||||||
|
"candidate": candidate?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Candidate {
|
||||||
|
Candidate({
|
||||||
|
this.candidate,
|
||||||
|
this.sdpMid,
|
||||||
|
this.sdpMLineIndex,
|
||||||
|
this.usernameFragment,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? candidate;
|
||||||
|
String? sdpMid;
|
||||||
|
int? sdpMLineIndex;
|
||||||
|
String? usernameFragment;
|
||||||
|
|
||||||
|
factory Candidate.fromRawJson(String str) => Candidate.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory Candidate.fromJson(Map<String, dynamic> json) => Candidate(
|
||||||
|
candidate: json["candidate"],
|
||||||
|
sdpMid: json["sdpMid"],
|
||||||
|
sdpMLineIndex: json["sdpMLineIndex"],
|
||||||
|
usernameFragment: json["usernameFragment"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"candidate": candidate,
|
||||||
|
"sdpMid": sdpMid,
|
||||||
|
"sdpMLineIndex": sdpMLineIndex,
|
||||||
|
"usernameFragment": usernameFragment,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,381 +1,233 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:mohem_flutter_app/classes/colors.dart';
|
import 'package:mohem_flutter_app/classes/colors.dart';
|
||||||
import 'package:mohem_flutter_app/classes/utils.dart';
|
import 'package:mohem_flutter_app/classes/utils.dart';
|
||||||
|
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
|
||||||
|
import 'package:mohem_flutter_app/main.dart';
|
||||||
import 'package:mohem_flutter_app/models/chat/call.dart';
|
import 'package:mohem_flutter_app/models/chat/call.dart';
|
||||||
|
import 'package:mohem_flutter_app/provider/chat_call_provider.dart';
|
||||||
|
import 'package:mohem_flutter_app/provider/chat_provider_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class IncomingCall extends StatefulWidget {
|
class IncomingCall extends StatefulWidget {
|
||||||
CallDataModel incomingCallData;
|
CallDataModel outGoingCallData;
|
||||||
bool? isVideoCall;
|
bool isVideoCall;
|
||||||
|
|
||||||
IncomingCall({Key? key, required this.incomingCallData, this.isVideoCall}) : super(key: key);
|
IncomingCall({Key? key, required this.outGoingCallData, required this.isVideoCall}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_IncomingCallState createState() => _IncomingCallState();
|
_IncomingCallState createState() => _IncomingCallState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IncomingCallState extends State<IncomingCall> with SingleTickerProviderStateMixin {
|
class _IncomingCallState extends State<IncomingCall> with SingleTickerProviderStateMixin {
|
||||||
AnimationController? _animationController;
|
late ChatCallProvider callProvider;
|
||||||
CameraController? _controller;
|
late ChatProviderModel chatProvider;
|
||||||
Future<void>? _initializeControllerFuture;
|
|
||||||
bool isCameraReady = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_animationController = AnimationController(
|
chatProvider = Provider.of<ChatProviderModel>(context, listen: false);
|
||||||
vsync: this,
|
callProvider = Provider.of<ChatCallProvider>(context, listen: false);
|
||||||
duration: const Duration(
|
init();
|
||||||
milliseconds: 500,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
//_runAnimation();
|
|
||||||
// connectSignaling();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => _runAnimation(),
|
|
||||||
);
|
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
widget.isVideoCall ? callProvider.isVideoCall = true : callProvider.isVideoCall = false;
|
||||||
|
callProvider.initLocalCamera(chatProvmodel: chatProvider, callData: widget.outGoingCallData, context: context, isIncomingCall: true);
|
||||||
|
callProvider.init();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: FutureBuilder<void>(
|
body: Consumer<ChatCallProvider>(builder: (BuildContext context, ChatCallProvider chatcp, Widget? child) {
|
||||||
future: _initializeControllerFuture,
|
if (chatcp.isCallEnded) Navigator.pop(context);
|
||||||
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
|
return Stack(
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
alignment: FractionalOffset.center,
|
||||||
return Stack(
|
children: <Widget>[
|
||||||
alignment: FractionalOffset.center,
|
if (widget.isVideoCall)
|
||||||
children: <Widget>[
|
Positioned.fill(
|
||||||
if (widget.isVideoCall!)
|
child: RTCVideoView(
|
||||||
Positioned.fill(
|
callProvider.localVideoRenderer,
|
||||||
child: AspectRatio(
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
aspectRatio: _controller!.value.aspectRatio,
|
),
|
||||||
child: CameraPreview(
|
),
|
||||||
_controller!,
|
Positioned.fill(
|
||||||
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MyColors.grey57Color.withOpacity(
|
||||||
|
0.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
Positioned.fill(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: ClipRect(
|
mainAxisSize: MainAxisSize.max,
|
||||||
child: BackdropFilter(
|
children: <Widget>[
|
||||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
40.height,
|
||||||
child: Container(
|
Row(
|
||||||
decoration: BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
color: MyColors.grey57Color.withOpacity(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
0.7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.all(21.0),
|
margin: const EdgeInsets.all(21.0),
|
||||||
child: Row(
|
child: Container(
|
||||||
children: <Widget>[
|
margin: const EdgeInsets.only(
|
||||||
Image.asset(
|
left: 10.0,
|
||||||
"assets/images/logos/main_mohemm_logo.png",
|
right: 10.0,
|
||||||
height: 70,
|
),
|
||||||
width: 70,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Container(
|
mainAxisSize: MainAxisSize.min,
|
||||||
margin: const EdgeInsets.only(
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
left: 10.0,
|
children: <Widget>[
|
||||||
right: 10.0,
|
SvgPicture.asset(
|
||||||
),
|
"assets/images/user.svg",
|
||||||
child: Column(
|
height: 70,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
width: 70,
|
||||||
mainAxisSize: MainAxisSize.min,
|
fit: BoxFit.cover,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: const <Widget>[
|
|
||||||
|
|
||||||
// todo @aamir, need to use extension mehtods
|
|
||||||
Text(
|
|
||||||
"Aamir Saleem Ahmad",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 21,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: MyColors.white,
|
|
||||||
letterSpacing: -1.26,
|
|
||||||
height: 23 / 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Calling...",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(
|
|
||||||
0xffC6C6C6,
|
|
||||||
),
|
|
||||||
letterSpacing: -0.48,
|
|
||||||
height: 23 / 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
10.height,
|
||||||
],
|
Text(
|
||||||
),
|
widget.outGoingCallData.receiverName.toString().replaceAll(".", " "),
|
||||||
),
|
style: const TextStyle(
|
||||||
// Container(
|
fontSize: 21,
|
||||||
// margin: const EdgeInsets.all(21.0),
|
fontWeight: FontWeight.bold,
|
||||||
// width: MediaQuery.of(context).size.width,
|
|
||||||
// decoration: cardRadius(15.0, color: MyColors.black, elevation: null),
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// mainAxisSize: MainAxisSize.min,
|
|
||||||
// children: [
|
|
||||||
// Container(
|
|
||||||
// padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 6.0),
|
|
||||||
// child: Text(
|
|
||||||
// "TranslationBase.of(context).appoInfo",
|
|
||||||
// style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.white, letterSpacing: -0.64, height: 23 / 12),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// Container(
|
|
||||||
// padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
|
||||||
// child: Text(
|
|
||||||
// "widget.incomingCallData.appointmentdate + widget.incomingCallData.appointmenttime",
|
|
||||||
// style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// Container(
|
|
||||||
// padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 21.0),
|
|
||||||
// child: Text(
|
|
||||||
// "widget.incomingCallData.clinicname",
|
|
||||||
// style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
const Spacer(),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
bottom: 70.0,
|
|
||||||
left: 49,
|
|
||||||
right: 49,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
RotationTransition(
|
|
||||||
turns: Tween(
|
|
||||||
begin: 0.0,
|
|
||||||
end: -.1,
|
|
||||||
)
|
|
||||||
.chain(
|
|
||||||
CurveTween(
|
|
||||||
curve: Curves.elasticIn,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.animate(
|
|
||||||
_animationController!,
|
|
||||||
),
|
|
||||||
child: RawMaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
_submit();
|
|
||||||
},
|
|
||||||
elevation: 2.0,
|
|
||||||
fillColor: MyColors.green2DColor,
|
|
||||||
padding: const EdgeInsets.all(
|
|
||||||
15.0,
|
|
||||||
),
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.call,
|
|
||||||
color: MyColors.white,
|
color: MyColors.white,
|
||||||
size: 35.0,
|
letterSpacing: -1.26,
|
||||||
|
height: 23 / 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Text(
|
||||||
RawMaterialButton(
|
"Ringing...",
|
||||||
onPressed: () {
|
style: TextStyle(
|
||||||
backToHome();
|
fontSize: 16,
|
||||||
},
|
fontWeight: FontWeight.w600,
|
||||||
elevation: 2.0,
|
color: Color(
|
||||||
fillColor: MyColors.redA3Color,
|
0xffC6C6C6,
|
||||||
padding: const EdgeInsets.all(
|
),
|
||||||
15.0,
|
letterSpacing: -0.48,
|
||||||
|
height: 23 / 24,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
shape: const CircleBorder(),
|
const SizedBox(
|
||||||
child: const Icon(
|
height: 2,
|
||||||
Icons.call_end,
|
|
||||||
color: MyColors.white,
|
|
||||||
size: 35.0,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
bottom: 70.0,
|
||||||
|
left: 49,
|
||||||
|
right: 49,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
if (widget.isVideoCall)
|
||||||
|
RawMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
callProvider.camOff();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: callProvider.isCamOff ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
15.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: Icon(
|
||||||
|
callProvider.isCamOff ? Icons.videocam_off : Icons.videocam,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 35.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
RawMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
callProvider.loudOn();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: callProvider.isLoudSpeaker ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
15.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.volume_up,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 35.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
callProvider.micOff();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: callProvider.isMicOff ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
15.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: Icon(
|
||||||
|
callProvider.isMicOff ? Icons.mic_off : Icons.mic,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 35.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
callProvider.endCall().then((value) {
|
||||||
|
if (value) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: MyColors.redA3Color,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
15.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.call_end,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 35.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
),
|
||||||
} else {
|
],
|
||||||
return const Center(
|
);
|
||||||
child: CircularProgressIndicator(),
|
}),
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _runAnimation() async {
|
|
||||||
List<CameraDescription> cameras = await availableCameras();
|
|
||||||
CameraDescription firstCamera = cameras[1];
|
|
||||||
_controller = CameraController(
|
|
||||||
firstCamera,
|
|
||||||
ResolutionPreset.medium,
|
|
||||||
);
|
|
||||||
_initializeControllerFuture = _controller!.initialize();
|
|
||||||
setState(() {});
|
|
||||||
// setAudioFile();
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
await _animationController!.forward();
|
|
||||||
await _animationController!.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submit() async {
|
|
||||||
try {
|
|
||||||
// backToHome();
|
|
||||||
// final roomModel = RoomModel(name: widget.incomingCallData.name, token: widget.incomingCallData.sessionId, identity: widget.incomingCallData.identity);
|
|
||||||
await _controller?.dispose();
|
|
||||||
|
|
||||||
// changeCallStatusAPI(4);
|
|
||||||
|
|
||||||
// if (_session != null && _signaling != null) {
|
|
||||||
// await Navigator.of(context).pushReplacement(
|
|
||||||
// MaterialPageRoute(
|
|
||||||
// // fullscreenDialog: true,
|
|
||||||
// builder: (BuildContext context) {
|
|
||||||
// // if (widget.incomingCallData.isWebRTC == "true") {
|
|
||||||
// return StartVideoCall(signaling: _signaling, session: _session);
|
|
||||||
//
|
|
||||||
// // else {
|
|
||||||
// // return OpenTokConnectCallPage(apiKey: OPENTOK_API_KEY, sessionId: widget.incomingCallData.sessionId, token: widget.incomingCallData.token);
|
|
||||||
// // }
|
|
||||||
//
|
|
||||||
// // return VideoCallWebPage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // Web WebRTC VideoCall
|
|
||||||
//
|
|
||||||
// // return CallHomePage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // App WebRTC VideoCall
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// // Invalid Params/Data
|
|
||||||
// Utils.showToast("Failed to establish connection with server");
|
|
||||||
// }
|
|
||||||
} catch (err) {
|
|
||||||
print(err);
|
|
||||||
// await PlatformExceptionAlertDialog(
|
|
||||||
// exception: err,
|
|
||||||
// ).show(context);
|
|
||||||
|
|
||||||
Utils.showToast(err.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// void changeCallStatusAPI(int sessionStatus) {
|
|
||||||
// LiveCareService service = new LiveCareService();
|
|
||||||
// service.endCallAPI(widget.incomingCallData.sessionId, sessionStatus, context).then((res) {}).catchError((err) {
|
|
||||||
// print(err);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
void backToHome() async {
|
|
||||||
// final connected = await signaling.declineCall(widget.incomingCallData.callerID, widget.incomingCallData.receiverID);
|
|
||||||
// LandingPage.isOpenCallPage = false;
|
|
||||||
// _signaling
|
|
||||||
_animationController!.dispose();
|
|
||||||
// player.stop();
|
|
||||||
// changeCallStatusAPI(3);
|
|
||||||
// _signaling.bye(_session, callRejected: true);
|
|
||||||
// _signaling.callDisconnected(_session, callRejected: true);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// void disposeAudioResources() async {
|
|
||||||
// await player.dispose();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void setAudioFile() async {
|
|
||||||
// player.stop();
|
|
||||||
// await player.setVolume(1.0); // full volume
|
|
||||||
// try {
|
|
||||||
// await player.setAsset('assets/sounds/ring_60Sec.mp3').then((value) {
|
|
||||||
// player.setLoopMode(LoopMode.one); // loop ring sound
|
|
||||||
// player.play();
|
|
||||||
// }).catchError((err) {
|
|
||||||
// print("Error: $err");
|
|
||||||
// });
|
|
||||||
// } catch (e) {
|
|
||||||
// print("Error: $e");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void connectSignaling({@required bool iAmCaller = false}) async {
|
|
||||||
// print("----------------- + Signaling Connection Started ---------------------------");
|
|
||||||
// var caller = widget.incomingCallData.callerID;
|
|
||||||
// var receiver = widget.incomingCallData.receiverID;
|
|
||||||
// var host = widget.incomingCallData.server;
|
|
||||||
//
|
|
||||||
// var selfRole = iAmCaller ? "Caller" : "Receiver";
|
|
||||||
// var selfId = iAmCaller ? caller : receiver;
|
|
||||||
// var selfUser = SocketUser(id: selfId, name: "$selfRole-$selfId", userAgent: DeviceInfo.userAgent, moreInfo: {});
|
|
||||||
//
|
|
||||||
// var remoteRole = !iAmCaller ? "Caller" : "Receiver";
|
|
||||||
// var remoteId = !iAmCaller ? caller : receiver;
|
|
||||||
// var remoteUser = SocketUser(id: remoteId, name: "$remoteRole-$remoteId", userAgent: DeviceInfo.userAgent, moreInfo: {});
|
|
||||||
//
|
|
||||||
// var sessionId = "$caller-$receiver";
|
|
||||||
// _session = SessionOneToOne(id: sessionId, local_user: selfUser, remote_user: remoteUser);
|
|
||||||
//
|
|
||||||
// _signaling = Signaling(host, session: _session);
|
|
||||||
// await _signaling.connect();
|
|
||||||
//
|
|
||||||
// if (_signaling.state == SignalingState.Open) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
BoxDecoration cardRadius(double radius, {required Color color, double? elevation}) {
|
BoxDecoration cardRadius(double radius, {required Color color, double? elevation}) {
|
||||||
return BoxDecoration(
|
return BoxDecoration(
|
||||||
shape: BoxShape.rectangle,
|
shape: BoxShape.rectangle,
|
||||||
color: color ?? Colors.white,
|
color: color ?? Colors.white,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(Radius.circular(radius)),
|
||||||
Radius.circular(radius),
|
boxShadow: <BoxShadow>[BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: elevation ?? 27, offset: const Offset(-2, 3))],
|
||||||
),
|
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
color: const Color(
|
|
||||||
0xff000000,
|
|
||||||
).withOpacity(
|
|
||||||
.05,
|
|
||||||
),
|
|
||||||
//spreadRadius: 5,
|
|
||||||
blurRadius: elevation ?? 27,
|
|
||||||
offset: const Offset(
|
|
||||||
-2,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,171 @@
|
|||||||
|
// import 'dart:async';
|
||||||
|
// import 'dart:io';
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
//
|
||||||
|
// class DraggableCam extends StatefulWidget {
|
||||||
|
// //final Size availableScreenSize;
|
||||||
|
// final Widget child;
|
||||||
|
// final double scaleFactor;
|
||||||
|
// // final Stream<bool> onButtonBarVisible;
|
||||||
|
// // final Stream<double> onButtonBarHeight;
|
||||||
|
//
|
||||||
|
// const DraggableCam({
|
||||||
|
// Key? key,
|
||||||
|
// //@required this.availableScreenSize,
|
||||||
|
// required this.child,
|
||||||
|
// // @required this.onButtonBarVisible,
|
||||||
|
// // @required this.onButtonBarHeight,
|
||||||
|
//
|
||||||
|
// /// The portion of the screen the DraggableWidget should use.
|
||||||
|
// this.scaleFactor = .25,
|
||||||
|
// }) : assert(scaleFactor != null && scaleFactor > 0 && scaleFactor <= .4),
|
||||||
|
// // assert(availableScreenSize != null),
|
||||||
|
// // assert(onButtonBarVisible != null),
|
||||||
|
// // assert(onButtonBarHeight != null),
|
||||||
|
// super(key: key);
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// _DraggablePublisherState createState() => _DraggablePublisherState();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// class _DraggablePublisherState extends State<DraggableCam> {
|
||||||
|
// bool _isButtonBarVisible = true;
|
||||||
|
// double _buttonBarHeight = 0;
|
||||||
|
// late double _width;
|
||||||
|
// late double _height;
|
||||||
|
// late double _top;
|
||||||
|
// late double _left;
|
||||||
|
// late double _viewPaddingTop;
|
||||||
|
// late double _viewPaddingBottom;
|
||||||
|
// final double _padding = 8.0;
|
||||||
|
// final Duration _duration300ms = const Duration(milliseconds: 300);
|
||||||
|
// final Duration _duration0ms = const Duration(milliseconds: 0);
|
||||||
|
// late Duration _duration;
|
||||||
|
// late StreamSubscription _streamSubscription;
|
||||||
|
// late StreamSubscription _streamHeightSubscription;
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void initState() {
|
||||||
|
// super.initState();
|
||||||
|
// _duration = _duration300ms;
|
||||||
|
// _width = widget.availableScreenSize.width * widget.scaleFactor;
|
||||||
|
// _height = _width * (widget.availableScreenSize.height / widget.availableScreenSize.width);
|
||||||
|
// _top = widget.availableScreenSize.height - (_buttonBarHeight + _padding) - _height;
|
||||||
|
// _left = widget.availableScreenSize.width - _padding - _width;
|
||||||
|
//
|
||||||
|
// _streamSubscription = widget.onButtonBarVisible.listen(_buttonBarVisible);
|
||||||
|
// _streamHeightSubscription = widget.onButtonBarHeight.listen(_getButtonBarHeight);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void didChangeDependencies() {
|
||||||
|
// var mediaQuery = MediaQuery.of(context);
|
||||||
|
// _viewPaddingTop = mediaQuery.viewPadding.top;
|
||||||
|
// _viewPaddingBottom = mediaQuery.viewPadding.bottom;
|
||||||
|
// super.didChangeDependencies();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void dispose() {
|
||||||
|
// _streamSubscription.cancel();
|
||||||
|
// _streamHeightSubscription.cancel();
|
||||||
|
// super.dispose();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// void _getButtonBarHeight(double height) {
|
||||||
|
// setState(() {
|
||||||
|
// _buttonBarHeight = height;
|
||||||
|
// _positionWidget();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// void _buttonBarVisible(bool visible) {
|
||||||
|
// if (!mounted) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setState(() {
|
||||||
|
// _isButtonBarVisible = visible;
|
||||||
|
// if (_duration == _duration300ms) {
|
||||||
|
// // only position the widget when we are not currently dragging it around
|
||||||
|
// _positionWidget();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return AnimatedPositioned(
|
||||||
|
// top: _top,
|
||||||
|
// left: _left,
|
||||||
|
// width: _width,
|
||||||
|
// height: _height,
|
||||||
|
// duration: _duration,
|
||||||
|
// child: Listener(
|
||||||
|
// onPointerDown: (_) => _duration = _duration0ms,
|
||||||
|
// onPointerMove: (PointerMoveEvent event) {
|
||||||
|
// setState(() {
|
||||||
|
// _left = (_left + event.delta.dx).roundToDouble();
|
||||||
|
// _top = (_top + event.delta.dy).roundToDouble();
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// onPointerUp: (_) => _positionWidget(),
|
||||||
|
// onPointerCancel: (_) => _positionWidget(),
|
||||||
|
// child: ClippedVideo(
|
||||||
|
// height: _height,
|
||||||
|
// width: _width,
|
||||||
|
// child: widget.child,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// double _getCurrentStatusBarHeight() {
|
||||||
|
// if (_isButtonBarVisible) {
|
||||||
|
// return _viewPaddingTop;
|
||||||
|
// }
|
||||||
|
// final _defaultViewPaddingTop = Platform.isIOS ? 20.0 : Platform.isAndroid ? 24.0 : 0.0;
|
||||||
|
// if (_viewPaddingTop > _defaultViewPaddingTop) {
|
||||||
|
// // There must be a hardware notch in the display.
|
||||||
|
// return _viewPaddingTop;
|
||||||
|
// }
|
||||||
|
// return 0.0;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// double _getCurrentButtonBarHeight() {
|
||||||
|
// if (_isButtonBarVisible) {
|
||||||
|
// return _buttonBarHeight + _viewPaddingBottom;
|
||||||
|
// }
|
||||||
|
// return _viewPaddingBottom;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// void _positionWidget() {
|
||||||
|
// // Determine the center of the object being dragged so we can decide
|
||||||
|
// // in which corner the object should be placed.
|
||||||
|
// var dx = (_width / 2) + _left;
|
||||||
|
// dx = dx < 0 ? 0 : dx >= widget.availableScreenSize.width ? widget.availableScreenSize.width - 1 : dx;
|
||||||
|
// var dy = (_height / 2) + _top;
|
||||||
|
// dy = dy < 0 ? 0 : dy >= widget.availableScreenSize.height ? widget.availableScreenSize.height - 1 : dy;
|
||||||
|
// final draggableCenter = Offset(dx, dy);
|
||||||
|
//
|
||||||
|
// setState(() {
|
||||||
|
// _duration = _duration300ms;
|
||||||
|
// if (Rect.fromLTRB(0, 0, widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||||
|
// // Top-left
|
||||||
|
// _top = _getCurrentStatusBarHeight() + _padding;
|
||||||
|
// _left = _padding;
|
||||||
|
// } else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, 0, widget.availableScreenSize.width, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||||
|
// // Top-right
|
||||||
|
// _top = _getCurrentStatusBarHeight() + _padding;
|
||||||
|
// _left = widget.availableScreenSize.width - _padding - _width;
|
||||||
|
// } else if (Rect.fromLTRB(0, widget.availableScreenSize.height / 2, widget.availableScreenSize.width / 2, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||||
|
// // Bottom-left
|
||||||
|
// _top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||||
|
// _left = _padding;
|
||||||
|
// } else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2, widget.availableScreenSize.width, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||||
|
// // Bottom-right
|
||||||
|
// _top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||||
|
// _left = widget.availableScreenSize.width - _padding - _width;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@ -0,0 +1,267 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:core';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:draggable_widget/draggable_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:mohem_flutter_app/classes/colors.dart';
|
||||||
|
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
|
||||||
|
import 'package:mohem_flutter_app/provider/chat_call_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class StartCallPage extends StatefulWidget {
|
||||||
|
RTCVideoRenderer localRenderer;
|
||||||
|
RTCVideoRenderer remoteRenderer;
|
||||||
|
|
||||||
|
StartCallPage({required this.localRenderer, required this.remoteRenderer});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_StartCallPageState createState() => _StartCallPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StartCallPageState extends State<StartCallPage> {
|
||||||
|
final dragController = DragController();
|
||||||
|
late ChatCallProvider callProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
callProvider = Provider.of<ChatCallProvider>(context, listen: false);
|
||||||
|
super.initState();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
callProvider.notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<ChatCallProvider>(builder: (BuildContext context, ChatCallProvider provider, Widget? child) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Stack(
|
||||||
|
alignment: FractionalOffset.center,
|
||||||
|
children: [
|
||||||
|
if (provider.isVideoCall)
|
||||||
|
Positioned.fill(
|
||||||
|
child: RTCVideoView(
|
||||||
|
widget.remoteRenderer,
|
||||||
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
|
key: const Key('remote'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (provider.isVideoCall)
|
||||||
|
DraggableWidget(
|
||||||
|
bottomMargin: 20,
|
||||||
|
topMargin: 40,
|
||||||
|
intialVisibility: true,
|
||||||
|
horizontalSpace: 20,
|
||||||
|
shadowBorderRadius: 50,
|
||||||
|
initialPosition: AnchoringPosition.topLeft,
|
||||||
|
dragController: dragController,
|
||||||
|
normalShadow: const BoxShadow(spreadRadius: 0.0, blurRadius: 0.0),
|
||||||
|
draggingShadow: const BoxShadow(spreadRadius: 0.0, blurRadius: 0.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 200,
|
||||||
|
width: 140,
|
||||||
|
child: RTCVideoView(
|
||||||
|
widget.localRenderer,
|
||||||
|
mirror: true,
|
||||||
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!provider.isVideoCall)
|
||||||
|
Positioned.fill(
|
||||||
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MyColors.grey57Color.withOpacity(
|
||||||
|
0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
40.height,
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(21.0),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
left: 10.0,
|
||||||
|
right: 10.0,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: <Widget>[
|
||||||
|
SvgPicture.asset(
|
||||||
|
"assets/images/user.svg",
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
10.height,
|
||||||
|
Text(
|
||||||
|
callProvider.outGoingCallData.receiverName!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 21,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: MyColors.white,
|
||||||
|
letterSpacing: -1.26,
|
||||||
|
height: 23 / 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
"On Call",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(
|
||||||
|
0xffC6C6C6,
|
||||||
|
),
|
||||||
|
letterSpacing: -0.48,
|
||||||
|
height: 23 / 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 20,
|
||||||
|
left: 40,
|
||||||
|
right: 40,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
// if (provider.isVideoCall)
|
||||||
|
RawMaterialButton(
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
callProvider.loudOn();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: callProvider.isLoudSpeaker ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.volume_up,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
provider.camOff();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: provider.isCamOff ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: Icon(
|
||||||
|
provider.isCamOff ? Icons.videocam_off : Icons.videocam,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
provider.switchCamera();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: provider.isFrontCamera ? Colors.grey : MyColors.green2DColor,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: Icon(
|
||||||
|
provider.isFrontCamera ? Icons.switch_camera_outlined : Icons.switch_camera,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
provider.micOff();
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: provider.isMicOff ? MyColors.green2DColor : Colors.grey,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: Icon(
|
||||||
|
provider.isMicOff ? Icons.mic_off : Icons.mic,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RawMaterialButton(
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
onPressed: () {
|
||||||
|
provider.endCall().then((value) {
|
||||||
|
if (value) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
elevation: 2.0,
|
||||||
|
fillColor: MyColors.redA3Color,
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.call_end,
|
||||||
|
color: MyColors.white,
|
||||||
|
size: 30.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue