Created
March 19, 2024 13:02
-
-
Save DhruvamUnikon/f40b22bfaeb43d279b028375b280567c to your computer and use it in GitHub Desktop.
Zego Video Call
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:io'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/scheduler.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:flutter/widgets.dart'; | |
import 'package:flutter_screenutil/flutter_screenutil.dart'; | |
import 'package:flutter_bloc/flutter_bloc.dart'; | |
import 'package:flutter_dotenv/flutter_dotenv.dart'; | |
import 'package:unikon/features/mentor/presentation/state/bloc/event_and_states.dart'; | |
import 'package:unikon/features/mentor/presentation/state/bloc/mentor_bloc.dart'; | |
import 'package:unikon/features/post/domain/entity/reaction/short_user_vm.dart'; | |
import 'package:unikon/utils/dependency/dependency_injection.dart'; | |
import 'package:unikon/utils/dimensions/gaps.dart'; | |
import 'package:unikon/utils/extensions/text/hardcoded.dart'; | |
import 'package:unikon/utils/images/constants.dart'; | |
import 'package:unikon/utils/null_checker/null_checker.dart'; | |
import 'package:unikon/utils/permissions/video_call_permissions.dart'; | |
import 'package:unikon/utils/theme/theme_colors.dart'; | |
import 'package:unikon/widget_library/animated_containers/call_hide_show_widget.dart'; | |
import 'package:unikon/widget_library/avatars/profile_avatar.dart'; | |
import 'package:unikon/widget_library/buttons/inverse_primary_button.dart'; | |
import 'package:unikon/widget_library/buttons/primary_button.dart'; | |
import 'package:unikon/widget_library/images/base_image.dart'; | |
import 'package:unikon/widget_library/scaffold/base_scaffold.dart'; | |
import 'package:unikon/widget_library/video_call/recorder/screen_recorder_utils.dart'; | |
import 'package:wakelock_plus/wakelock_plus.dart'; | |
import 'package:zego_express_engine/zego_express_engine.dart'; | |
class AdvancedVideoCallHolder extends StatelessWidget { | |
final ShortUserVM user; | |
final ShortUserVM secondUser; | |
final String roomId; | |
final bool isHost; | |
final bool isVideo; | |
final DateTime endTime; | |
final num duration; | |
final String sessionId; | |
final bool recordingConsentGiven; | |
const AdvancedVideoCallHolder({ | |
super.key, | |
required this.user, | |
required this.roomId, | |
this.isHost = false, | |
required this.isVideo, | |
required this.secondUser, | |
required this.endTime, | |
required this.duration, | |
required this.sessionId, | |
required this.recordingConsentGiven, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return BlocProvider<MentorBloc>( | |
create: (context) => getIt.get<MentorBloc>(), | |
child: BlocListener<MentorBloc, MentorState>( | |
listener: (context, state) { | |
if (state is MentorError) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text( | |
state.response.message, | |
), | |
), | |
); | |
} | |
}, | |
child: _AdvancedVideoCallScreen( | |
user: user, | |
secondUser: secondUser, | |
roomId: roomId, | |
isHost: isHost, | |
isVideo: isVideo, | |
endTime: endTime, | |
duration: duration, | |
sessionId: sessionId, | |
recordingConsentGiven: recordingConsentGiven, | |
), | |
), | |
); | |
} | |
} | |
class _AdvancedVideoCallScreen extends StatefulWidget { | |
final ShortUserVM user; | |
final ShortUserVM secondUser; | |
final String roomId; | |
final bool isHost; | |
final bool isVideo; | |
final DateTime endTime; | |
final num duration; | |
final String sessionId; | |
final bool recordingConsentGiven; | |
const _AdvancedVideoCallScreen({ | |
super.key, | |
required this.user, | |
required this.secondUser, | |
required this.roomId, | |
required this.isHost, | |
required this.isVideo, | |
required this.endTime, | |
required this.duration, | |
required this.sessionId, | |
required this.recordingConsentGiven, | |
}); | |
@override | |
State<_AdvancedVideoCallScreen> createState() => | |
_AdvancedVideoCallScreenState(); | |
} | |
class _AdvancedVideoCallScreenState extends State<_AdvancedVideoCallScreen> { | |
Widget? localView; | |
Widget? remoteView; | |
int? localViewID; | |
int? remoteViewID; | |
Timer? _timer; | |
bool snackBarShown = false; | |
bool isRecordingStarted = false; | |
bool callButtonVisible = true; | |
ZegoScreenRecordUtils? screenRecordUtils; | |
bool isVideoOn = true; | |
@override | |
Widget build(BuildContext context) { | |
return BaseScaffold( | |
body: widget.isVideo ? videoCallLayout() : audioCallLayout(), | |
); | |
} | |
Widget audioCallLayout() { | |
return Stack( | |
children: [ | |
SingleChildScrollView( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
SizedBox( | |
height: 40.sp, | |
), | |
SizedBox( | |
height: 20.sp, | |
width: 102.sp, | |
child: const BaseImageWidget( | |
imageUri: ImageConstants.unikonCallLogo, | |
), | |
), | |
g20Box, | |
g20Box, | |
Center( | |
child: ProfileAvatarWidget( | |
radius: 90.sp, | |
firstName: widget.secondUser.firstName, | |
lastName: widget.secondUser.lastName, | |
showInitialsIfEmpty: false, | |
url: widget.secondUser.profilePictureUrl, | |
), | |
), | |
SizedBox( | |
height: 50.sp, | |
), | |
Text( | |
'${widget.secondUser.firstName} ${widget.secondUser.lastName}', | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 28.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
if (widget.secondUser.currentDesignation != null && | |
widget.secondUser.currentDesignation!.isNotEmpty && | |
widget.secondUser.currentOrganisation != null && | |
widget.secondUser.currentOrganisation!.isNotEmpty) | |
Text( | |
'${widget.secondUser.currentDesignation} @ ${widget.secondUser.currentOrganisation}', | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
), | |
if (widget.secondUser.city != null) g4Box, | |
if (widget.secondUser.city != null) | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
BaseImageWidget( | |
imageUri: ImageConstants.callLocationIcon, | |
size: Size(12.sp, 12.sp), | |
), | |
g4Box, | |
Text( | |
NullChecker.buildCityAndCountry(widget.secondUser.city, | |
widget.secondUser.country?.name), | |
style: TextStyle( | |
color: const Color(0xFFCCCCCC), | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w300, | |
), | |
), | |
], | |
), | |
const SizedBox( | |
height: 36, | |
), | |
Container( | |
padding: const EdgeInsets.symmetric( | |
horizontal: 8, | |
vertical: 6, | |
), | |
decoration: ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(10.sp), | |
), | |
), | |
child: CallTimerWidget( | |
duration: widget.duration, | |
endTime: widget.endTime, | |
), | |
), | |
], | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: SizedBox( | |
width: double.infinity, | |
child: BaseImageWidget( | |
size: Size(double.infinity, 120.sp), | |
imageUri: ImageConstants.audioCallBottomLayout, | |
color: const Color(0xff242840), | |
), | |
), | |
), | |
Positioned( | |
bottom: 80.sp, | |
left: 0, | |
right: 0, | |
child: CircleAvatar( | |
radius: 30.sp, | |
backgroundColor: Colors.red, | |
child: IconButton( | |
onPressed: () async { | |
try { | |
final navigator = Navigator.of(context); | |
if (mounted) { | |
await logoutRoom(); | |
stopListenEvent(); | |
navigator.pop(); | |
} | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
}, | |
icon: Icon( | |
Icons.call_end, | |
size: 30.sp, | |
color: Colors.white, | |
), | |
), | |
), | |
), | |
Positioned( | |
bottom: 20.sp, | |
left: 0, | |
right: 0, | |
child: Text( | |
'Your call is secure'.hardcoded, | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
textAlign: TextAlign.center, | |
), | |
), | |
Positioned( | |
bottom: 32.sp, | |
left: 24.sp, | |
child: SizedBox( | |
height: 40.sp, | |
width: 40.sp, | |
child: const AdvancedCallMuteButton(), | |
), | |
), | |
Positioned( | |
bottom: 32.sp, | |
right: 24.sp, | |
child: Container( | |
width: 40.sp, | |
height: 40.sp, | |
decoration: ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(12), | |
), | |
), | |
child: const BaseImageWidget( | |
imageUri: ImageConstants.callMoreIcon, | |
), | |
), | |
), | |
], | |
); | |
} | |
Widget videoCallLayout() { | |
return GestureDetector( | |
onTap: () { | |
if (mounted) { | |
setState(() { | |
callButtonVisible = !callButtonVisible; | |
}); | |
} | |
}, | |
child: Container( | |
color: Colors.transparent, | |
child: Stack( | |
children: [ | |
Column( | |
children: [ | |
if (localView != null) | |
Expanded( | |
child: Stack( | |
children: [ | |
if (isVideoOn) | |
localView! | |
else | |
Center( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
BaseImageWidget( | |
imageUri: ImageConstants.callVideoTurnedOffIcon, | |
size: Size(80.sp, 80.sp), | |
), | |
g20Box, | |
Text( | |
'Video paused', | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 16.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
], | |
), | |
), | |
Positioned( | |
bottom: 0, | |
left: 0, | |
child: _UserDetailsWidget(user: widget.user), | |
), | |
if (!widget.isVideo) | |
Center( | |
child: ProfileAvatarWidget( | |
radius: 50, | |
firstName: widget.user.firstName, | |
lastName: widget.user.lastName, | |
), | |
), | |
], | |
), | |
) | |
else | |
const Expanded(child: SizedBox()), | |
Container( | |
color: Colors.teal, | |
width: MediaQuery.of(context).size.width, | |
height: 32.sp, | |
child: const Center( | |
child: BaseImageWidget( | |
imageUri: ImageConstants.unikonBrandingLogo, | |
), | |
), | |
), | |
if (remoteView != null) | |
Expanded( | |
child: Stack( | |
children: [ | |
remoteView!, | |
Positioned( | |
bottom: 0, | |
left: 0, | |
child: _UserDetailsWidget(user: widget.secondUser), | |
), | |
if (!widget.isVideo) | |
Center( | |
child: ProfileAvatarWidget( | |
radius: 50, | |
firstName: widget.secondUser.firstName, | |
lastName: widget.secondUser.lastName, | |
), | |
), | |
], | |
), | |
), | |
AnimatedContainer( | |
duration: const Duration(milliseconds: 300), | |
height: callButtonVisible ? 70.sp : 0, | |
color: Colors.black, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: [ | |
SizedBox( | |
height: 30.sp, | |
width: 30.sp, | |
child: const AdvancedCallMuteButton( | |
showBorder: false, | |
), | |
), | |
// camera off button | |
VideoOnOffButton( | |
onVideoOnOff: (isOn) { | |
if (mounted) { | |
setState(() { | |
isVideoOn = isOn; | |
}); | |
} | |
}, | |
), | |
CircleAvatar( | |
radius: 14.sp, | |
backgroundColor: Colors.red, | |
child: IconButton( | |
onPressed: () async { | |
try { | |
final navigator = Navigator.of(context); | |
if (mounted) { | |
await logoutRoom(); | |
stopListenEvent(); | |
navigator.pop(); | |
} | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
}, | |
icon: Icon( | |
Icons.call_end, | |
size: 14.sp, | |
color: Colors.white, | |
), | |
), | |
), | |
const CallCameraFlipButton(), | |
BaseImageWidget( | |
imageUri: ImageConstants.callMoreIcon, | |
size: Size(30.sp, 24.sp), | |
), | |
], | |
), | |
), | |
], | |
), | |
Positioned( | |
bottom: MediaQuery.of(context).size.height / 20, | |
left: 0, | |
right: 0, | |
child: CallHideShowWidget( | |
show: callButtonVisible, | |
onHidden: (show) { | |
setState(() { | |
callButtonVisible = show; | |
}); | |
}, | |
children: const [], | |
), | |
), | |
Container( | |
margin: EdgeInsets.only( | |
top: 20.sp, | |
left: 16.sp, | |
right: 0, | |
), | |
padding: const EdgeInsets.symmetric( | |
horizontal: 8, | |
vertical: 2, | |
), | |
decoration: ShapeDecoration( | |
color: const Color(0xB2232323), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(16.sp), | |
), | |
), | |
child: CallTimerWidget( | |
duration: widget.duration, | |
endTime: widget.endTime, | |
hourColor: Colors.white, | |
minuteColor: Colors.red, | |
secondColor: Colors.red, | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
void startListenEvent() { | |
// Callback for updates on the status of other users in the room. | |
// Users can only receive callbacks when the isUserStatusNotify property of ZegoRoomConfig is set to `true` when logging in to the room (loginRoom). | |
ZegoExpressEngine.onRoomUserUpdate = | |
(roomID, updateType, List<ZegoUser> userList) {}; | |
// Callback for updates on the status of the streams in the room. | |
ZegoExpressEngine.onRoomStreamUpdate = | |
(roomID, updateType, List<ZegoStream> streamList, extendedData) { | |
for (var element in streamList) { | |
debugPrint('Stream ID ---- ---- ---- - ---: ${element.user.userID}'); | |
} | |
if (updateType == ZegoUpdateType.Add) { | |
for (final stream in streamList) { | |
startPlayStream(stream.streamID); | |
} | |
} else { | |
for (final stream in streamList) { | |
stopPlayStream(stream.streamID); | |
} | |
} | |
}; | |
// Callback for updates on the current user's room connection status. | |
ZegoExpressEngine.onRoomStateUpdate = | |
(roomID, state, errorCode, extendedData) { | |
debugPrint( | |
'onRoomStateUpdate: roomID: $roomID, state: ${state.name}, errorCode: $errorCode, extendedData: $extendedData'); | |
}; | |
// Callback for updates on the current user's stream publishing changes. | |
ZegoExpressEngine.onPublisherStateUpdate = | |
(streamID, state, errorCode, extendedData) { | |
debugPrint( | |
'onPublisherStateUpdate: streamID: $streamID, state: ${state.name}, errorCode: $errorCode, extendedData: $extendedData'); | |
}; | |
ZegoExpressEngine.onCapturedDataRecordStateUpdate = | |
(state, errorCode, configID, channel) { | |
print('_AdvancedVideoCallHolderState.startListenEvent $state'); | |
debugPrint( | |
'onCapturedDataRecordStateUpdate: state: ${state.name}, errorCode: $errorCode, configID: $configID'); | |
}; | |
} | |
void stopListenEvent() { | |
ZegoExpressEngine.onRoomUserUpdate = null; | |
ZegoExpressEngine.onRoomStreamUpdate = null; | |
ZegoExpressEngine.onRoomStateUpdate = null; | |
ZegoExpressEngine.onPublisherStateUpdate = null; | |
} | |
Future<ZegoRoomLoginResult> loginRoom() async { | |
// The value of `userID` is generated locally and must be globally unique. | |
final user = ZegoUser('user_${widget.user.id}', | |
'${widget.user.firstName} ${widget.user.lastName}'); | |
// The value of `roomID` is generated locally and must be globally unique. | |
final roomID = widget.roomId; | |
// onRoomUserUpdate callback can be received when "isUserStatusNotify" parameter value is "true". | |
ZegoRoomConfig roomConfig = ZegoRoomConfig.defaultConfig() | |
..isUserStatusNotify = true | |
..maxMemberCount = 2; | |
// log in to a room | |
// Users must log in to the same room to call each other. | |
return ZegoExpressEngine.instance | |
.loginRoom(roomID, user, config: roomConfig) | |
.then((ZegoRoomLoginResult loginRoomResult) async { | |
debugPrint( | |
'loginRoom: errorCode:${loginRoomResult.errorCode}, extendedData:${loginRoomResult.extendedData}'); | |
if (loginRoomResult.errorCode == 0) { | |
startPreview(); | |
startPublish(); | |
final bloc = context.read<MentorBloc>(); | |
if (widget.isVideo && mounted && !widget.recordingConsentGiven) { | |
final response = await showModalBottomSheet( | |
context: context, | |
enableDrag: false, | |
builder: (context) => | |
RecordingConsentSheet(sessionId: widget.sessionId), | |
); | |
// make an api call to start recording the call | |
if (response != null && response == true) { | |
bloc.add(StartCloudRecordingEvent(widget.sessionId)); | |
} | |
} | |
} else { | |
ScaffoldMessenger.of(context).showSnackBar(SnackBar( | |
content: Text('loginRoom failed: ${loginRoomResult.errorCode}'))); | |
} | |
return loginRoomResult; | |
}); | |
} | |
Future<ZegoRoomLogoutResult> logoutRoom() async { | |
stopPreview(); | |
stopPublish(); | |
return ZegoExpressEngine.instance.logoutRoom(widget.roomId); | |
} | |
Future<void> startPreview() async { | |
await ZegoExpressEngine.instance.createCanvasView((viewID) { | |
localViewID = viewID; | |
ZegoCanvas previewCanvas = | |
ZegoCanvas(viewID, viewMode: ZegoViewMode.AspectFill); | |
ZegoExpressEngine.instance.startPreview(canvas: previewCanvas); | |
}).then((canvasViewWidget) { | |
setState(() { | |
localView = canvasViewWidget; | |
}); | |
}); | |
} | |
Future<void> stopPreview() async { | |
ZegoExpressEngine.instance.stopPreview(); | |
if (localViewID != null) { | |
await ZegoExpressEngine.instance.destroyCanvasView(localViewID!); | |
if (mounted) { | |
setState(() { | |
localViewID = null; | |
localView = null; | |
}); | |
} | |
} | |
} | |
Future<void> startPublish() async { | |
// After calling the `loginRoom` method, call this method to publish streams. | |
// The StreamID must be unique in the room. | |
String streamID = '${widget.roomId}_${widget.user.id}_call'; | |
return ZegoExpressEngine.instance.startPublishingStream(streamID); | |
} | |
Future<void> stopPublish() async { | |
return ZegoExpressEngine.instance.stopPublishingStream(); | |
} | |
Future<void> startPlayStream(String streamID) async { | |
// Start to play streams. Set the view for rendering the remote streams. | |
await ZegoExpressEngine.instance.createCanvasView((viewID) { | |
remoteViewID = viewID; | |
ZegoCanvas canvas = ZegoCanvas(viewID, viewMode: ZegoViewMode.AspectFill); | |
ZegoExpressEngine.instance.startPlayingStream(streamID, canvas: canvas); | |
}).then((canvasViewWidget) { | |
setState(() => remoteView = canvasViewWidget); | |
}); | |
} | |
Future<void> stopPlayStream(String streamID) async { | |
ZegoExpressEngine.instance.stopPlayingStream(streamID); | |
if (remoteViewID != null) { | |
ZegoExpressEngine.instance.destroyCanvasView(remoteViewID!); | |
setState(() { | |
remoteViewID = null; | |
remoteView = null; | |
}); | |
} | |
} | |
@override | |
void initState() { | |
super.initState(); | |
WakelockPlus.enable(); | |
ZegoExpressEngine.createEngineWithProfile( | |
ZegoEngineProfile( | |
int.parse(dotenv.env['ZEGO_APP_ID'.hardcoded]!), | |
widget.isVideo ? ZegoScenario.Default : ZegoScenario.StandardVoiceCall, | |
appSign: dotenv.env['ZEGO_APP_SIGN'.hardcoded]!, | |
), | |
).then((value) { | |
ZegoExpressEngine.instance.enableCamera(widget.isVideo); | |
initTimer(); | |
VideoCallPermissions.requestPermission(takeStorage: true) | |
.then((value) async { | |
if (value) { | |
await loginRoom(); | |
startListenEvent(); | |
} else { | |
try { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text( | |
'Permission not granted', | |
), | |
), | |
); | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
} | |
}); | |
}); | |
} | |
void initTimer() async { | |
final currentTime = DateTime.now(); | |
int maxTime = 60 * 5; | |
if (widget.duration * 60 < maxTime) { | |
maxTime = (widget.duration * 60 / 2).round(); | |
} | |
int remaining = widget.endTime.difference(currentTime).inSeconds; | |
_timer = Timer.periodic( | |
const Duration(seconds: 1), | |
(Timer timer) { | |
remaining--; | |
// show a snackbar at 5 minutes and 2 minutes | |
if (remaining == 60 * 5 && widget.duration > 15) { | |
snackBarShown = true; | |
try { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text("Your session is going to end in 5 minutes"), | |
), | |
); | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
} else if (remaining == 60 * 2) { | |
try { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text("Your session is going to end in 2 minutes"), | |
), | |
); | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
} else if (remaining == 60 * 0.5) { | |
try { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text("Your session is going to end in 30 seconds"), | |
), | |
); | |
} catch (e) { | |
debugPrint(e.toString()); | |
} | |
} else if (remaining == 0) { | |
timer.cancel(); | |
logoutRoom(); | |
stopListenEvent(); | |
if (mounted) { | |
Navigator.of(context).pop(); | |
} | |
} | |
}, | |
); | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
logoutRoom(); | |
stopListenEvent(); | |
_timer?.cancel(); | |
ZegoExpressEngine.destroyEngine(); | |
WakelockPlus.disable(); | |
} | |
} | |
class CallTimerWidget extends StatefulWidget { | |
final num duration; | |
final DateTime endTime; | |
final Color? hourColor; | |
final Color? minuteColor; | |
final Color? secondColor; | |
const CallTimerWidget({ | |
super.key, | |
required this.duration, | |
required this.endTime, | |
this.hourColor, | |
this.minuteColor, | |
this.secondColor, | |
}); | |
@override | |
State<CallTimerWidget> createState() => _CallTimerWidgetState(); | |
} | |
class _CallTimerWidgetState extends State<CallTimerWidget> { | |
late Duration durationToRun; | |
@override | |
void initState() { | |
durationToRun = widget.endTime.difference(DateTime.now()); | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// create a timer widget that uses end time and duration | |
// | |
return TweenAnimationBuilder<Duration>( | |
duration: durationToRun, | |
tween: Tween(begin: durationToRun, end: const Duration(seconds: 0)), | |
onEnd: () { | |
print('Timer ended'); | |
}, | |
builder: (BuildContext context, Duration value, Widget? child) { | |
final hours = value.inHours.toString().padLeft(2, '0'); | |
final minutes = | |
value.inMinutes.remainder(60).toString().padLeft(2, '0'); | |
final seconds = | |
value.inSeconds.remainder(60).toString().padLeft(2, '0'); | |
return Padding( | |
padding: const EdgeInsets.symmetric(vertical: 5), | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text( | |
'$hours:', | |
style: TextStyle( | |
color: widget.hourColor ?? Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
Text( | |
'$minutes:', | |
style: TextStyle( | |
color: widget.minuteColor ?? Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
Text( | |
seconds, | |
style: TextStyle( | |
color: widget.secondColor ?? Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
} | |
class _UserDetailsWidget extends StatelessWidget { | |
final ShortUserVM user; | |
const _UserDetailsWidget({super.key, required this.user}); | |
@override | |
Widget build(BuildContext context) { | |
// add a gradient to the background so that the text is more readable | |
return Container( | |
padding: g20Padding, | |
width: MediaQuery.of(context).size.width, | |
decoration: const BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.bottomCenter, | |
end: Alignment.topCenter, | |
colors: [Colors.black, Colors.transparent], | |
), | |
), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
'${user.firstName} ${user.lastName}', | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w500, | |
), | |
), | |
if (user.currentDesignation != null && | |
user.currentDesignation!.isNotEmpty && | |
user.currentOrganisation != null && | |
user.currentOrganisation!.isNotEmpty) | |
Text( | |
'${user.currentDesignation} @ ${user.currentOrganisation}', | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
), | |
if (user.city != null) g4Box, | |
if (user.city != null) | |
Row( | |
children: [ | |
BaseImageWidget( | |
imageUri: ImageConstants.callLocationIcon, | |
size: Size(12.sp, 12.sp), | |
), | |
g4Box, | |
Text( | |
NullChecker.buildCityAndCountry( | |
user.city, user.country?.name), | |
style: TextStyle( | |
color: const Color(0xFFCCCCCC), | |
fontSize: 12.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w300, | |
), | |
), | |
], | |
), | |
], | |
), | |
); | |
} | |
} | |
class AdvancedCallMuteButton extends StatefulWidget { | |
final bool showBorder; | |
const AdvancedCallMuteButton({super.key, this.showBorder = true}); | |
@override | |
State<AdvancedCallMuteButton> createState() => _AdvancedCallMuteButtonState(); | |
} | |
class _AdvancedCallMuteButtonState extends State<AdvancedCallMuteButton> { | |
bool isMuted = false; | |
@override | |
void initState() { | |
ZegoExpressEngine.instance.isMicrophoneMuted().then((value) { | |
if (mounted) { | |
setState(() { | |
isMuted = value; | |
}); | |
} | |
}); | |
super.initState(); | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () async { | |
final scaffoldMessenger = ScaffoldMessenger.of(context); | |
try { | |
if (mounted) { | |
HapticFeedback.mediumImpact(); | |
setState(() { | |
isMuted = !isMuted; | |
}); | |
} | |
await ZegoExpressEngine.instance.muteMicrophone(isMuted); | |
} catch (e) { | |
scaffoldMessenger.showSnackBar( | |
SnackBar( | |
content: Text( | |
'Permission not granted'.hardcoded, | |
), | |
), | |
); | |
} | |
}, | |
child: Container( | |
decoration: widget.showBorder | |
? ShapeDecoration( | |
color: Colors.white.withOpacity(0.20000000298023224), | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(12), | |
), | |
) | |
: null, | |
padding: widget.showBorder ? const EdgeInsets.all(8) : null, | |
child: isMuted | |
? BaseImageWidget( | |
imageUri: ImageConstants.callMuteMicIcon, | |
size: Size(24.sp, 24.sp), | |
color: widget.showBorder ? null : const Color(0xFF808080), | |
) | |
: BaseImageWidget( | |
imageUri: ImageConstants.callMicIcon, | |
size: Size(24.sp, 24.sp), | |
color: widget.showBorder ? null : const Color(0xFF808080), | |
), | |
), | |
); | |
} | |
} | |
class CallCameraFlipButton extends StatefulWidget { | |
const CallCameraFlipButton({super.key}); | |
@override | |
State<CallCameraFlipButton> createState() => _CallCameraFlipButtonState(); | |
} | |
class _CallCameraFlipButtonState extends State<CallCameraFlipButton> { | |
bool isFrontCamera = true; | |
@override | |
void dispose() { | |
ZegoExpressEngine.instance.useFrontCamera(true); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: Colors.black.withOpacity(0.30000001192092896), | |
), | |
child: IconButton( | |
onPressed: () async { | |
final scaffoldMessenger = ScaffoldMessenger.of(context); | |
try { | |
setState(() { | |
isFrontCamera = !isFrontCamera; | |
}); | |
await ZegoExpressEngine.instance.useFrontCamera(isFrontCamera); | |
} catch (e) { | |
scaffoldMessenger.showSnackBar( | |
SnackBar( | |
content: Text( | |
'Permission not granted'.hardcoded, | |
), | |
), | |
); | |
} | |
}, | |
icon: Icon( | |
isFrontCamera | |
? Icons.flip_camera_ios | |
: Icons.flip_camera_ios_outlined, | |
color: const Color(0xFF808080), | |
size: 30.sp, | |
), | |
), | |
); | |
} | |
} | |
class VideoOnOffButton extends StatefulWidget { | |
final Function(bool) onVideoOnOff; | |
const VideoOnOffButton({super.key, required this.onVideoOnOff}); | |
@override | |
State<VideoOnOffButton> createState() => _VideoOnOffButtonState(); | |
} | |
class _VideoOnOffButtonState extends State<VideoOnOffButton> { | |
bool isVideoOn = true; | |
@override | |
void initState() { | |
ZegoExpressEngine.instance.enableCamera(isVideoOn); | |
SchedulerBinding.instance.addPostFrameCallback((timeStamp) { | |
if (mounted) { | |
widget.onVideoOnOff(isVideoOn); | |
} | |
}); | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () async { | |
final scaffoldMessenger = ScaffoldMessenger.of(context); | |
try { | |
setState(() { | |
isVideoOn = !isVideoOn; | |
}); | |
widget.onVideoOnOff(isVideoOn); | |
await ZegoExpressEngine.instance.enableCamera(isVideoOn); | |
} catch (e) { | |
scaffoldMessenger.showSnackBar( | |
SnackBar( | |
content: Text( | |
'Permission not granted'.hardcoded, | |
), | |
), | |
); | |
} | |
}, | |
child: Container( | |
padding: const EdgeInsets.all(8), | |
child: isVideoOn | |
? BaseImageWidget( | |
imageUri: ImageConstants.callVideoIcon, | |
size: Size(30.sp, 30.sp), | |
) | |
: BaseImageWidget( | |
imageUri: ImageConstants.callVideoOffIcon, | |
size: Size(30.sp, 30.sp), | |
), | |
), | |
); | |
} | |
} | |
class RecordingConsentSheet extends StatelessWidget { | |
final String sessionId; | |
const RecordingConsentSheet({super.key, required this.sessionId}); | |
@override | |
Widget build(BuildContext context) { | |
return BlocProvider<MentorBloc>( | |
create: (context) => getIt.get<MentorBloc>(), | |
child: Stack( | |
clipBehavior: Clip.none, | |
children: [ | |
Container( | |
padding: EdgeInsets.only( | |
bottom: MediaQuery.of(context).viewInsets.bottom + 20, | |
left: 20, | |
right: 20, | |
top: 20, | |
), | |
decoration: const BoxDecoration( | |
color: Colors.black, | |
borderRadius: BorderRadius.only( | |
topLeft: Radius.circular(20), | |
topRight: Radius.circular(20), | |
), | |
), | |
child: SingleChildScrollView( | |
child: Column( | |
children: [ | |
Align( | |
alignment: Alignment.center, | |
child: Container( | |
height: 3, | |
width: 24, | |
decoration: BoxDecoration( | |
color: ThemeColor.appBarTextColor(context), | |
borderRadius: BorderRadius.circular(10), | |
), | |
), | |
), | |
g24Box, | |
SizedBox( | |
height: 120.sp, | |
width: 120.sp, | |
child: const BaseImageWidget( | |
imageUri: ImageConstants.recordingConsentIcon, | |
), | |
), | |
g40Box, | |
Text( | |
'Allow Session Recording'.hardcoded, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 18.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w700, | |
height: 0.06, | |
), | |
), | |
g16Box, | |
Text( | |
'This will allow you to download the session later. Your acceptance also permits us to post this session on UniTube.' | |
.hardcoded, | |
textAlign: TextAlign.center, | |
style: TextStyle( | |
color: const Color(0xFFCCCCCC), | |
fontSize: 14.sp, | |
fontFamily: 'Poppins', | |
fontWeight: FontWeight.w400, | |
), | |
), | |
g40Box, | |
Row( | |
children: [ | |
Expanded( | |
child: InversePrimaryBorderedButton( | |
isLoading: false, | |
label: 'Decline', | |
onPressed: () => Navigator.pop(context, false), | |
color: ThemeColor.greyColor(context), | |
), | |
), | |
g16Box, | |
Expanded( | |
child: BlocConsumer<MentorBloc, MentorState>( | |
listener: (context, state) { | |
if (state is MentorError) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text( | |
state.response.message, | |
), | |
), | |
); | |
} else if (state is RecordingConsentUpdated) { | |
Navigator.pop(context, true); | |
} | |
}, | |
builder: (context, state) { | |
return PrimaryButton( | |
isLoading: state is MentorLoading, | |
label: 'Accept', | |
onPressed: () { | |
final bloc = context.read<MentorBloc>(); | |
bloc.add( | |
UpdateRecordingConsent(sessionId, true)); | |
}, | |
); | |
}, | |
), | |
), | |
], | |
), | |
], | |
), | |
), | |
), | |
Positioned( | |
top: -30.sp, | |
right: 16.sp, | |
child: Container( | |
alignment: Alignment.topRight, | |
color: Colors.white.withOpacity(0), | |
child: GestureDetector( | |
onTap: () { | |
Navigator.pop(context); | |
}, | |
child: Icon( | |
Icons.close, | |
size: 20, | |
color: ThemeColor.greyColor(context).withOpacity(.8), | |
), | |
), | |
), | |
) | |
], | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment