Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DhruvamUnikon/f40b22bfaeb43d279b028375b280567c to your computer and use it in GitHub Desktop.
Save DhruvamUnikon/f40b22bfaeb43d279b028375b280567c to your computer and use it in GitHub Desktop.
Zego Video Call
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