diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6a1c47e..4f7ef74 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,9 +30,6 @@ - @@ -91,6 +88,12 @@ + + + + + + @@ -115,6 +118,7 @@ value == null); - log("uri: ${Uri.parse(url.trim())}"); - - log("body: ${json.encode(body)}"); final bool networkStatus = await Utils.checkConnection(bypassConnectionCheck: bypassConnectionCheck); @@ -204,7 +200,7 @@ class ApiClientImp implements ApiClient { final int statusCode = response.statusCode; log("uri: ${Uri.parse(url.trim())}"); log("body: ${json.encode(body)}"); - log("response.body: ${response.body}"); + // log("response.body: ${response.body}"); // log("response.body: ${response.body}"); if (statusCode < 200 || statusCode >= 400) { onFailure('Error While Fetching data', statusCode, failureType: StatusCodeFailure("Error While Fetching data")); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 417519f..b92461e 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -710,7 +710,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -749,7 +749,7 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; - SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; @@ -759,7 +759,7 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; - SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; @@ -779,7 +779,7 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; - SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; @@ -789,7 +789,7 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; applePayMerchantId = "merchant.com.hmgwebservices.uat"; - SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; + SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; GET_TAMARA_INSTALLMENTS_URL = "https://epharmacy.hmg.com/tamara/Home/getinstallments"; GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 5fccc6e..613fba1 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -175,6 +175,8 @@ class AppAssets { static const String comprehensiveCheckup = '$svgBasePath/comprehensive_checkup.svg'; static const String all_payment_method = '$svgBasePath/all_payment_method.svg'; static const String ic_rrt_vehicle = '$svgBasePath/ic_rrt_vehicle.svg'; + static const String doctor_profile_rating_icon = '$svgBasePath/doctor_profile_rating_icon.svg'; + static const String doctor_profile_reviews_icon = '$svgBasePath/doctor_profile_reviews_icon.svg'; //bottom navigation// diff --git a/lib/core/cache_consts.dart b/lib/core/cache_consts.dart index 66b38d4..bcbb185 100644 --- a/lib/core/cache_consts.dart +++ b/lib/core/cache_consts.dart @@ -74,4 +74,7 @@ class CacheConst { static const String patientOccupationList = 'patient-occupation-list'; static const String hasEnabledQuickLogin = 'has-enabled-quick-login'; static const String quickLoginEnabled = 'quick-login-enabled'; + + static const String zoomRoomID = 'zoom-room-id'; + static String isAppOpenedFromCall = "is_app_opened_from_call"; } diff --git a/lib/core/utils/jwt.dart b/lib/core/utils/jwt.dart new file mode 100644 index 0000000..f82c4e1 --- /dev/null +++ b/lib/core/utils/jwt.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:hmg_patient_app_new/core/utils/zoom_config.dart'; + +String makeId(int length) { + String result = ""; + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result += characters[Random().nextInt(charactersLength)]; + } + return result; +} + +String generateJwt(String sessionName, String roleType) { + try { + var iat = DateTime.now(); + var exp = DateTime.now().add(Duration(days: 2)); + final jwt = JWT( + { + 'app_key': configs["ZOOM_SDK_KEY"], + 'version': 1, + 'user_identity': makeId(10), + 'iat': (iat.millisecondsSinceEpoch / 1000).round(), + 'exp': (exp.millisecondsSinceEpoch / 1000).round(), + 'tpc': sessionName, + 'role_type': int.parse(roleType), + 'cloud_recording_option': 1, + }, + ); + var token = jwt.sign(SecretKey(configs["ZOOM_SDK_SECRET"])); + return token; + } catch (e) { + print(e); + return ''; + } +} diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index 2e0d7c3..ee05335 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -8,14 +8,22 @@ import 'package:firebase_messaging/firebase_messaging.dart' as fir; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_callkit_incoming/entities/android_params.dart'; +import 'package:flutter_callkit_incoming/entities/call_kit_params.dart'; +import 'package:flutter_callkit_incoming/entities/ios_params.dart'; +import 'package:flutter_callkit_incoming/entities/notification_params.dart'; +import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_ios_voip_kit_karmm/call_state_type.dart'; import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart'; // import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/utils/local_notifications.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:uuid/uuid.dart'; import '../cache_consts.dart'; @@ -36,16 +44,83 @@ Future backgroundMessageHandler(dynamic message) async { callPage(String sessionID, String token) async {} _incomingCall(Map data) async { + print('the value of the _incomingCall remote message is $data'); + // LandingPage.incomingCallData = IncomingCallData.fromJson(data); // var dataItem = await AppSharedPreferences().getObject('call_data'); // if(dataItem != null ) return; // to stop repeated attempt to invoke the call // if (LandingPage.isOpenCallPage == false) { // LandingPage.isOpenCallPage = true; + + String roomID = data['session_id'] ?? ''; + + // GetIt.instance().saveString(key: CacheConst.zoomRoomID, value: roomID); + // GetIt.instance().saveBool(key: CacheConst.isAppOpenedFromCall, value: true); + + Utils.saveStringFromPrefs(CacheConst.zoomRoomID, roomID); + Utils.saveBoolFromPrefs(CacheConst.isAppOpenedFromCall, true); + WidgetsFlutterBinding.ensureInitialized(); - // var _currentUuid = Uuid().v4(); - // await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); - // } - // LandingPage.isOpenCallPage = false; + + var _currentUuid = Uuid().v4(); + CallKitParams callKitParams = CallKitParams( + id: _currentUuid, + nameCaller: "Dr Al Habib", + appName: 'Dr Al Habib', + avatar: 'https://play-lh.googleusercontent.com/FBNNpxb7m6eM6wtW7MV1Ffp6OXOGLI38q47zcvP29OCYA1yhYH5mZzl5itZi0TgOyZpG', + // handle: LandingPage.incomingCallData.name, + type: 1, + textAccept: 'Accept', + textDecline: 'Decline', + missedCallNotification: NotificationParams( + showNotification: true, + isShowCallback: false, + subtitle: 'Missed call', + callbackText: '', + ), + callingNotification: const NotificationParams( + showNotification: false, + isShowCallback: true, + subtitle: 'Calling...', + callbackText: 'Hang Up', + ), + duration: 30000, + extra: {'userId': '1a2b3c4d'}, + headers: {'apiKey': 'Abc@123!', 'platform': 'flutter'}, + android: const AndroidParams( + isImportant: true, + isShowFullLockedScreen: true, + isCustomNotification: true, + isShowLogo: false, + logoUrl: "https://play-lh.googleusercontent.com/FBNNpxb7m6eM6wtW7MV1Ffp6OXOGLI38q47zcvP29OCYA1yhYH5mZzl5itZi0TgOyZpG", + ringtonePath: 'system_ringtone_default', + backgroundColor: '#ED1C2B', + backgroundUrl: 'https://saudipedia.com/en/saudipediaen/uploads/images/2024/08/02/97358.jpg', + actionColor: '#4CAF50', + textColor: '#000000', + incomingCallNotificationChannelName: "Incoming Call", + missedCallNotificationChannelName: "Missed Call", + isShowCallID: false), + ios: IOSParams( + iconName: 'Meena Health', + handleType: 'generic', + supportsVideo: true, + maximumCallGroups: 2, + maximumCallsPerCallGroup: 1, + audioSessionMode: 'default', + audioSessionActive: true, + audioSessionPreferredSampleRate: 44100.0, + audioSessionPreferredIOBufferDuration: 0.005, + supportsDTMF: true, + supportsHolding: true, + supportsGrouping: false, + supportsUngrouping: false, + ringtonePath: 'system_ringtone_default', + ), + ); + FlutterCallkitIncoming.unsilenceEvents(); + await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); + await Future.delayed(Duration(milliseconds: 500)); } @@ -224,7 +299,9 @@ class PushNotificationHandler { print('🎈 example: onDidAcceptIncomingCall $uuid - $callerId'); await voIPKit.acceptIncomingCall(callerState: CallStateType.calling); await voIPKit.callConnected(); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(Duration(seconds: 2)).then((val) async { + await voIPKit.endCall(); + }); // Navigator.pushNamed( // locator().navigatorKey.currentContext!, @@ -232,8 +309,6 @@ class PushNotificationHandler { // arguments: CallArguments("hoover-dam", "123", "Patient", "40", "1", false), // ); - await voIPKit.endCall(); - // Navigator.pushNamed(navigatorKey.currentContext!, VIDEO_CALL_SCREEN, // arguments: VideoArgus( // reservationId: int.parse(callerId), token: null, isVideo: true)); diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index e2c11d4..f1e1593 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -807,7 +807,11 @@ class Utils { static Future checkConnection({bool bypassConnectionCheck = false}) async { if (bypassConnectionCheck) return true; List connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult.contains(ConnectivityResult.mobile) || connectivityResult.contains(ConnectivityResult.wifi)) { + if (connectivityResult.contains(ConnectivityResult.mobile) || + connectivityResult.contains(ConnectivityResult.wifi) || + connectivityResult.contains(ConnectivityResult.ethernet) || + connectivityResult.contains(ConnectivityResult.bluetooth) || + connectivityResult.contains(ConnectivityResult.vpn)) { return true; } else { return false; diff --git a/lib/core/utils/zoom_config.dart b/lib/core/utils/zoom_config.dart new file mode 100644 index 0000000..09370c0 --- /dev/null +++ b/lib/core/utils/zoom_config.dart @@ -0,0 +1,4 @@ +const Map configs = { + 'ZOOM_SDK_KEY': 'b9T74nhfTg-ioP9urm970A', + 'ZOOM_SDK_SECRET': 'KOzmjBNXQ1f4IPHpnngfL29uZvJMufSy2Fk8', +}; diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 250453d..c2a4a87 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -301,14 +301,14 @@ extension EmailValidator on String { fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); - Widget toText28({Color? color, bool isBold = false, double? height, bool isCenter = false}) => Text( + Widget toText28({Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( height: height ?? 23 / 28, color: color ?? AppColors.blackColor, fontSize: 28.f, - letterSpacing: -1, + letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 5d5fc71..bcc88ff 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -349,8 +349,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { result.fold( (failure) async { isDoctorsListLoading = false; + if (onError != null) onError("No doctors found for the search criteria".needTranslation); - onError!("No doctors found for the search criteria".needTranslation); notifyListeners(); }, (apiResponse) { diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index 7e2d7c9..1baa81b 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -151,18 +151,15 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { if (patientLiveCareHistoryList.isNotEmpty) { if (patientLiveCareHistoryList[0].callStatus! < 4) { countdownDuration = Duration(seconds: 0); - durationNotifier.dispose(); patientHasPendingLiveCareRequest = true; countdownDuration = Duration(minutes: patientLiveCareHistoryList.first.watingtimeInteger!, seconds: 0); durationNotifier = ValueNotifier(countdownDuration); startTimer(); } else { patientHasPendingLiveCareRequest = false; - timer.cancel(); } } else { patientHasPendingLiveCareRequest = false; - timer.cancel(); } notifyListeners(); if (onSuccess != null) { diff --git a/lib/features/insurance/insurance_view_model.dart b/lib/features/insurance/insurance_view_model.dart index 8460bd4..30f4c26 100644 --- a/lib/features/insurance/insurance_view_model.dart +++ b/lib/features/insurance/insurance_view_model.dart @@ -78,10 +78,12 @@ class InsuranceViewModel extends ChangeNotifier { // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { isInsuranceLoading = false; + isInsuranceDataToBeLoaded = false; notifyListeners(); }, (apiResponse) { if (apiResponse.messageStatus == 2) { + isInsuranceDataToBeLoaded = false; // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { patientInsuranceList = apiResponse.data!; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 079471c..24aa244 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -77,14 +77,14 @@ class LabViewModel extends ChangeNotifier { required this.navigationService}); initLabProvider() { - if (isLabNeedToLoad) { + // if (isLabNeedToLoad) { patientLabOrders.clear(); filteredLabOrders.clear(); labOrderTests.clear(); isLabOrdersLoading = true; isLabResultsLoading = true; getPatientLabOrders(); - } + // } notifyListeners(); } @@ -94,7 +94,7 @@ class LabViewModel extends ChangeNotifier { } Future getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { - if (!isLabNeedToLoad) return; + // if (!isLabNeedToLoad) return; isLabOrdersLoading = true; patientLabOrders.clear(); @@ -289,7 +289,7 @@ class LabViewModel extends ChangeNotifier { ); } - Future getPatientLabResult(PatientLabOrdersResponseModel laborder, String procedureName, String testDescription) async { + Future getPatientLabResult(PatientLabOrdersResponseModel laborder, String procedureName, String testDescription, String unitOfMeasure) async { LoaderBottomSheet.showLoader(); mainLabResults.clear(); filteredGraphValues.clear(); @@ -339,6 +339,7 @@ class LabViewModel extends ChangeNotifier { } catch (e) {} }); LabResult recentResult = recentThree.first; + recentResult.uOM = unitOfMeasure; checkIfGraphShouldBeDisplayed(recentResult); recentResult.verifiedOn = resultDate(DateUtil.convertStringToDate(recentResult.verifiedOnDateTime!)); // filteredGraphValues = [filteredGraphValues.first]; diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 5d2ae5c..7bc9c62 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -124,19 +124,47 @@ class AppointmentCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.network( - isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, - width: 63.h, - height: 63.h, - fit: BoxFit.cover, + Stack( + children: [ + Image.network( + isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, + width: 63.w, + height: 63.h, + fit: BoxFit.cover, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + width: 63.w, + height: 20, + color: AppColors.textColorLight.withValues(alpha: 0.25), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.rating_icon, + width: 12.w, + height: 12.h, + fit: BoxFit.contain, + ), + SizedBox(width: 4.w), + isLoading ? "Rating".toText12() : patientAppointmentHistoryResponseModel.decimalDoctorRate.toString().toText12(), + ], + ), + ), + ) + ], ).circle(100).toShimmer2(isShow: isLoading), - SizedBox(height: 12.h), - AppCustomChipWidget( - icon: AppAssets.rating_icon, - iconColor: AppColors.ratingColorYellow, - labelText: isLoading ? "Rating" : "Rating: ${patientAppointmentHistoryResponseModel.decimalDoctorRate}".needTranslation) - .toShimmer2(isShow: isLoading), + // SizedBox(height: 12.h), + // AppCustomChipWidget( + // icon: AppAssets.rating_icon, + // iconColor: AppColors.ratingColorYellow, + // labelText: isLoading ? "Rating" : "Rating: ${patientAppointmentHistoryResponseModel.decimalDoctorRate}".needTranslation) + // .toShimmer2(isShow: isLoading), ], ), SizedBox(width: 16.h), @@ -163,6 +191,12 @@ class AppointmentCard extends StatelessWidget { ? 'Cardiology' : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}", ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + isIconPNG: true, + icon: getIt.get().getAuthenticatedUser()?.gender == 1 ? AppAssets.male_img : AppAssets.femaleImg, + iconSize: 18.h, + labelText: isLoading ? 'Cardiology' : "Patient: ${getIt.get().getAuthenticatedUser()!.firstName!}", + ).toShimmer2(isShow: isLoading), // if (!isFromMedicalReport) // AppCustomChipWidget( // icon: AppAssets.appointment_time_icon, @@ -278,7 +312,6 @@ class AppointmentCard extends StatelessWidget { clinicId: patientAppointmentHistoryResponseModel.clinicID, onSuccess: (value) async { if (value) { - print("Doctor is available"); await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( diff --git a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart index 9f06a4d..69cdca3 100644 --- a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart +++ b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart @@ -79,15 +79,15 @@ class AskDoctorRequestTypeSelect extends StatelessWidget { onPressed: () { Navigator.pop(context); }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, icon: AppAssets.cancel, - iconColor: AppColors.whiteColor, + iconColor: AppColors.primaryRedColor, borderRadius: 12.r, - iconSize: 14.h, - fontSize: 14.f, - height: 40.h, + iconSize: 16.5.h, + fontSize: 16.f, + height: 56.h, ), ), SizedBox(width: 8.h), @@ -120,14 +120,15 @@ class AskDoctorRequestTypeSelect extends StatelessWidget { LoaderBottomSheet.hideLoader(); }); }, - backgroundColor: AppColors.bgGreenColor, - borderColor: AppColors.bgGreenColor, - textColor: Colors.white, + backgroundColor: AppColors.bgGreenColor.withValues(alpha: 0.20), + borderColor: AppColors.bgGreenColor.withValues(alpha: 0.0), + textColor: AppColors.bgGreenColor, + iconColor: AppColors.bgGreenColor, icon: AppAssets.confirm, - iconSize: 14.h, + iconSize: 16.5.h, borderRadius: 12.r, - fontSize: 14.f, - height: 40.h, + fontSize: 16.f, + height: 56.h, ), ), ], diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 7e5f4a1..0fa995d 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -154,7 +155,7 @@ class LoginScreenState extends State { required BuildContext context, required TextEditingController? phoneNumberController, required AuthenticationViewModel authViewModel, - }) { + }) async { AppState appState = getIt(); context.showBottomSheet( isScrollControlled: true, diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index a66ea4d..8a0f7e7 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -2,20 +2,25 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_config.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart' show RegionBottomSheetBody; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart'; @@ -86,6 +91,111 @@ class _BookAppointmentPageState extends State { ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 24.h), getSelectedTabData(bookAppointmentsVM.selectedTabIndex), + SizedBox(height: 24.h), + "Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + Consumer(builder: (context, myAppointmentsVM, child) { + return myAppointmentsVM.isPatientMyDoctorsLoading + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: true, radius: 50.r), + SizedBox(height: 8.h), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), + ], + ) + : myAppointmentsVM.patientMyDoctorsList.isEmpty + ? Container( + width: SizeConfig.screenWidth, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: "You don't have any completed visits yet".needTranslation, + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ).paddingSymmetrical(24.w, 0.h) + : SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: myAppointmentsVM.patientMyDoctorsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + SizedBox( + width: 80.w, + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), + ), + ); + }), ], ); }), diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 1223842..c3f4ad9 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -48,6 +48,7 @@ class DoctorProfilePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network( bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!, @@ -59,9 +60,12 @@ class DoctorProfilePage extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") - .toString() - .toText28(isBold: true), + SizedBox( + width: 220.h, + child: ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") + .toString() + .toText24(isBold: true), + ), (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first : "") .toString() .toText18(weight: FontWeight.w500, color: AppColors.primaryRedColor), @@ -80,18 +84,49 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(height: 12.h), Wrap( direction: Axis.horizontal, - spacing: 3.h, + spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( - icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, - labelText: "Rating: ${bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate}".needTranslation, + labelText: "Branch: ${bookAppointmentsViewModel.doctorsProfileResponseModel.projectName}".needTranslation, ), AppCustomChipWidget( - icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, - labelText: "Reviews: ${bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate}".needTranslation, + labelText: "Clinic: ${bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription}".needTranslation, + ), + ], + ), + SizedBox(height: 24.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.doctor_profile_rating_icon, + width: 48.w, + height: 48.h, + fit: BoxFit.contain, + ), + SizedBox(height: 16.h), + "Ratings".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate.toString().toText16(isBold: true, color: AppColors.textColor), + ], + ), + SizedBox(width: 36.w), + Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.doctor_profile_reviews_icon, + width: 48.w, + height: 48.h, + fit: BoxFit.contain, + ), + SizedBox(height: 16.h), + "Reviews".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate.toString().toText16(isBold: true, color: AppColors.textColor), + ], ), ], ), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index af7d053..b586da1 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -13,13 +13,16 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -71,37 +74,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), SizedBox(height: 8.h), - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), - ], - ), - ], - ), - ), - ), - SizedBox(height: 24.h), - "Clinic Information".needTranslation.toText16(isBold: true), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 32.h, height: 32.h, fit: BoxFit.contain), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (appState.isArabic() - ? immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceNameN - : immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceName)! - .toText16(isBold: true), - // SizedBox(height: 8.h), - // AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + Wrap( + direction: Axis.horizontal, + spacing: 3.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old".needTranslation), + AppCustomChipWidget( + labelText: + "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceName)!}"), + ], + ), ], ), ], @@ -220,15 +203,81 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { isSaudiCurrency: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar"), ], ).paddingSymmetrical(24.h, 0.h), - CustomButton( - text: LocaleKeys.payNow.tr(context: context), - onPressed: () async { - await askVideoCallPermission().then((val) { - if (val) { - Navigator.of(context).push( - CustomPageRoute( - page: ImmediateLiveCarePaymentPage(), - ), + (immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total == "0.0") + ? CustomButton( + text: LocaleKeys.confirmLiveCare.tr(context: context), + onPressed: () async { + await askVideoCallPermission().then((val) async { + if (val) { + LoaderBottomSheet.showLoader(loadingText: "Confirming LiveCare request, Please wait...".needTranslation); + + await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare("${appState.getAuthenticatedUser()!.patientId}${DateTime.now().millisecondsSinceEpoch}"); + await immediateLiveCareViewModel.getPatientLiveCareHistory(); + LoaderBottomSheet.hideLoader(); + if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred...".needTranslation), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } else { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: + "LiveCare requires Camera, Microphone, Location & Notifications permissions to enable virtual consultation between patient & doctor, Please allow these to proceed." + .needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: AppColors.whiteColor, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.livecare_book_icon, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h) + : CustomButton( + text: LocaleKeys.payNow.tr(context: context), + onPressed: () async { + await askVideoCallPermission().then((val) { + if (val) { + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePaymentPage(), + ), ); } else { showCommonBottomSheetWithoutHeight( @@ -236,16 +285,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { context, child: Utils.getWarningWidget( loadingText: - "LiveCare requires Camera, Microphone & Location permissions to enable virtual consultation between patient & doctor, Please allow these to proceed.".needTranslation, - isShowActionButtons: true, - onCancelTap: () { - Navigator.pop(context); - }, - onConfirmTap: () async { - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, + "LiveCare requires Camera, Microphone, Location & Notifications permissions to enable virtual consultation between patient & doctor, Please allow these to proceed." + .needTranslation, + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, isCloseButtonVisible: true, ); } @@ -272,12 +322,9 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } Future askVideoCallPermission() async { - Map statuses = await [ - Permission.camera, - Permission.microphone, - ].request(); + Map statuses = await [Permission.camera, Permission.microphone, Permission.notification].request(); - if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted) { + if (statuses[Permission.camera] == PermissionStatus.granted && statuses[Permission.microphone] == PermissionStatus.granted && statuses[Permission.notification] == PermissionStatus.granted) { // Camera permission granted return true; } else { diff --git a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart index 8181d25..15598f3 100644 --- a/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart +++ b/lib/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart @@ -66,35 +66,6 @@ class _SelectImmediateLiveCareClinicPageState extends State(builder: (context, immediateLiveCareVM, child) { return Column( children: [ - // SizedBox(height: 16.h), - // TextInputWidget( - // labelText: LocaleKeys.search.tr(context: context), - // hintText: LocaleKeys.clinicName.tr(context: context), - // controller: searchEditingController, - // isEnable: true, - // prefix: null, - // autoFocus: false, - // isBorderAllowed: false, - // keyboardType: TextInputType.text, - // focusNode: textFocusNode, - // suffix: searchEditingController.text.isNotEmpty - // ? GestureDetector( - // onTap: () { - // searchEditingController.clear(); - // bookAppointmentsViewModel.filterClinics(""); - // textFocusNode.unfocus(); - // }, - // child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - // ) - // : null, - // onChange: (value) { - // bookAppointmentsViewModel.filterClinics(value!); - // }, - // padding: EdgeInsets.symmetric( - // vertical: ResponsiveExtension(10).h, - // horizontal: ResponsiveExtension(15).h, - // ), - // ), ListView.separated( padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, diff --git a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart index 7385c6b..3913f10 100644 --- a/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart +++ b/lib/presentation/book_appointment/livecare/widgets/livecare_clinic_card.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_mode import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; class LiveCareClinicCard extends StatelessWidget { LiveCareClinicCard({super.key, required this.liveCareClinicListResponseModel, required this.isLoading, required this.immediateLiveCareViewModel}); @@ -34,20 +35,12 @@ class LiveCareClinicCard extends StatelessWidget { children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Utils.buildSvgWithAssets(icon: AppAssets.generic_clinic_icon, width: 24.h, height: 24.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading), - Column( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.livecare_online_icon, - width: 16.h, - height: 16.h, - fit: BoxFit.contain, - iconColor: liveCareClinicListResponseModel.isOnline == 1 ? AppColors.successColor : AppColors.primaryRedColor) - .toShimmer2(isShow: isLoading), - SizedBox(height: 4.h), - liveCareClinicListResponseModel.isOnline == 1 - ? LocaleKeys.online.tr(context: context).toText10(isBold: true, color: AppColors.successColor).toShimmer2(isShow: isLoading) - : "Offline".toText10(isBold: true, color: AppColors.primaryRedColor).toShimmer2(isShow: isLoading), - ], + AppCustomChipWidget( + icon: AppAssets.livecare_online_icon, + iconColor: AppColors.whiteColor, + richText: (liveCareClinicListResponseModel.isOnline == 1 ? LocaleKeys.online.tr(context: context) : "Offline".needTranslation).toText10(isBold: true, color: AppColors.whiteColor), + backgroundColor: liveCareClinicListResponseModel.isOnline == 1 ? AppColors.successColor : AppColors.primaryRedColor, + textColor: AppColors.whiteColor, ), ]), SizedBox(height: 8.h), diff --git a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart index 226ac50..e4d69b1 100644 --- a/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart +++ b/lib/presentation/book_appointment/livecare/widgets/select_livecare_call_type.dart @@ -1,10 +1,14 @@ import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/widgets/medical_file_card.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -16,46 +20,30 @@ class SelectLiveCareCallType extends StatelessWidget { @override Widget build(BuildContext context) { //TODO: Replace with actual icons - return GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - mainAxisExtent: 130, - ), - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - shrinkWrap: true, + return Column( children: [ - MedicalFileCard( - label: "Video Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.eye_result_icon, - isLargeText: true, - iconSize: 36.h, + checkInOptionCard( + AppAssets.checkin_location_icon, + "Video Call".needTranslation, + "View your nearest HMG locations".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(1); }), - MedicalFileCard( - label: "Audio Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.allergy_info_icon, - isLargeText: true, - iconSize: 36.h, + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + "Audio Call".needTranslation, + "Provide your feedback on our services".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(2); }), - MedicalFileCard( - label: "Phone Call".needTranslation, - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.vaccine_info_icon, - isLargeText: true, - iconSize: 36.h, + SizedBox(height: 16.h), + checkInOptionCard( + AppAssets.checkin_location_icon, + "Phone Call".needTranslation, + "Live chat option with HMG".needTranslation, ).onPress(() { Navigator.of(context).pop(); immediateLiveCareViewModel.setLiveCareSelectedCallType(3); @@ -63,4 +51,45 @@ class SelectLiveCareCallType extends StatelessWidget { ], ); } + + Widget checkInOptionCard(String icon, String title, String subTitle) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(isBold: true, color: AppColors.textColor), + subTitle.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ), + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.forward_arrow_icon_small, + iconColor: AppColors.blackColor, + width: 18.h, + height: 13.h, + fit: BoxFit.contain, + ), + ), + ], + ), + ], + ).paddingAll(16.h), + ); + } } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index fe0268d..47f8118 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -40,6 +40,8 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -93,13 +95,13 @@ class _LandingPageState extends State { if (appState.isAuthenticated) { habibWalletVM.initHabibWalletProvider(); habibWalletVM.getPatientBalanceAmount(); + immediateLiveCareViewModel.initImmediateLiveCare(); + immediateLiveCareViewModel.getPatientLiveCareHistory(); myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false); myAppointmentsViewModel.getPatientMyDoctors(); prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); - immediateLiveCareViewModel.initImmediateLiveCare(); - immediateLiveCareViewModel.getPatientLiveCareHistory(); emergencyServicesViewModel.checkPatientERAdvanceBalance(); } }); @@ -140,10 +142,19 @@ class _LandingPageState extends State { text: LocaleKeys.loginOrRegister.tr(context: context), onPressed: () async { await authVM.onLoginPressed(); + + // Navigator.pushReplacementNamed( + // // context, + // context, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("test123", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, @@ -203,8 +214,8 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); }), SizedBox(height: 16.h), - Consumer( - builder: (context, myAppointmentsVM, child) { + Consumer2( + builder: (context, myAppointmentsVM, immediateLiveCareVM, child) { return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -248,14 +259,14 @@ class _LandingPageState extends State { indicatorLayout: PageIndicatorLayout.COLOR, axisDirection: AxisDirection.right, controller: _controller, - itemHeight: 270.h, + itemHeight: 255.h, pagination: SwiperPagination( alignment: Alignment.bottomCenter, - margin: EdgeInsets.only(top: 250.h + 8 + 24), + margin: EdgeInsets.only(top: 240.h + 8 + 24), builder: DotSwiperPaginationBuilder(color: Color(0xffD9D9D9), activeColor: AppColors.blackBgColor), ), itemBuilder: (BuildContext context, int index) { - return (immediateLiveCareViewModel.patientHasPendingLiveCareRequest && index == 0) + return (immediateLiveCareVM.patientHasPendingLiveCareRequest && index == 0) ? Column( children: [ SizedBox(height: 12.h), @@ -279,7 +290,7 @@ class _LandingPageState extends State { Row( children: [ AppCustomChipWidget( - labelText: immediateLiveCareViewModel.patientLiveCareHistoryList[0].stringCallStatus, + labelText: immediateLiveCareVM.patientLiveCareHistoryList[0].stringCallStatus, backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20), textColor: AppColors.alertColor, ), @@ -287,7 +298,7 @@ class _LandingPageState extends State { AppCustomChipWidget( icon: AppAssets.appointment_calendar_icon, labelText: DateUtil.formatDateToDate( - DateUtil.convertStringToDate(immediateLiveCareViewModel.patientLiveCareHistoryList[0].arrivalTime), false)), + DateUtil.convertStringToDate(immediateLiveCareVM.patientLiveCareHistoryList[0].arrivalTime), false)), ], ), Utils.buildSvgWithAssets(icon: AppAssets.waiting_icon, width: 24.h, height: 24.h), @@ -297,7 +308,7 @@ class _LandingPageState extends State { SizedBox(height: 10.h), "Hala ${appState.getAuthenticatedUser()!.firstName}!!!".needTranslation.toText16(isBold: true), SizedBox(height: 8.h), - "Your turn is after ${immediateLiveCareViewModel.patientLiveCareHistoryList[0].patCount} patients.".toText14(isBold: true), + "Your turn is after ${immediateLiveCareVM.patientLiveCareHistoryList[0].patCount} patients.".toText14(isBold: true), SizedBox(height: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -305,7 +316,7 @@ class _LandingPageState extends State { "Expected waiting time: ".toText12(isBold: true), SizedBox(height: 8.h), ValueListenableBuilder( - valueListenable: immediateLiveCareViewModel.durationNotifier, + valueListenable: immediateLiveCareVM.durationNotifier, builder: (context, duration, child) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -561,7 +572,7 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: ServicesPage())); }), ], - ).paddingSymmetrical(24.h, 0.h), + ).paddingSymmetrical(24.w, 0.h), SizedBox( height: 340.h, child: ListView.separated( @@ -613,7 +624,7 @@ class _LandingPageState extends State { cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: true); setState(() {}); await Future.delayed(Duration(milliseconds: 3000)).then((value) { - Navigator.pop(context); + if (mounted) Navigator.pop(context); }); }); }, diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 184f713..81c507f 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { // labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!), ""); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 8893ffd..3f05443 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -26,7 +26,7 @@ class LabResultList extends StatelessWidget { shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { var labItem = list[index]; return LabOrderResultItem(onTap: () { - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!); + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!, labItem.uOM ?? ""); }, tests: labItem, index: index, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 2330d0d..67eae63 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -59,115 +59,72 @@ class LabResultDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - recentLabResult.testCode ?? "", - style: TextStyle( - fontSize: 32, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.textColor, - letterSpacing: -2), - ), - Text( - "Result of ${recentLabResult.verifiedOn ?? ""}".needTranslation, - style: TextStyle( - fontSize: 12, - fontFamily: 'Poppins', - fontWeight: FontWeight.w500, - color: AppColors.greyTextColor, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + (recentLabResult.testCode ?? "").toText28(isBold: true, color: AppColors.textColor, letterSpacing: -2), + Visibility( + visible: recentLabResult.checkIfGraphShouldBeDisplayed() == true, + child: Column( + spacing: 6.h, + children: [ + context + .read() + .getSeverityText( + recentLabResult.calculatedResultFlag ?? "", + ) + .tr() + .toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), + Utils.buildSvgWithAssets( + icon: AppAssets.lab_result_indicator, + width: 21, + height: 23, + iconColor: context.read().getColor( + recentLabResult.calculatedResultFlag ?? "", + )), + ], + ), + ), + ], ), + SizedBox(height: 4.h), + ("Result of ${recentLabResult.verifiedOn ?? ""}".needTranslation).toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), ], ), - //todo change the text color according to the provided test values Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [ Expanded( child: Row( - mainAxisSize: MainAxisSize.max, children: [ Flexible( - child: Text( - "${recentLabResult.resultValue} ${recentLabResult.uOM}" ?? - "", - style: TextStyle( - fontSize: 24.f, - fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + child: ("${recentLabResult.resultValue} ${recentLabResult.uOM}" ?? "").toText28( + isBold: true, color: context.read().getColor( recentLabResult.calculatedResultFlag ?? "", - ), - letterSpacing: -2, - ), - overflow: TextOverflow.ellipsis, // prevent overflow - maxLines: 1, - softWrap: false, - ), + ), + letterSpacing: -2), ), SizedBox(width: 4.h,), - Expanded( - flex: 2, - child: Visibility( - visible: recentLabResult.referanceRange != null, - child: Text( - "(Reference range ${recentLabResult.referanceRange})".needTranslation, - style: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w500, - fontFamily: 'Poppins', - color: AppColors.greyTextColor, - ), - // overflow: TextOverflow.ellipsis, - // maxLines: 2, - softWrap: true, - ), - ), - ), - ], - ), - ), - Visibility( - visible: - recentLabResult.checkIfGraphShouldBeDisplayed() == true, - child: Column( - spacing: 6.h, - children: [ - context - .read() - .getSeverityText( - recentLabResult.calculatedResultFlag ?? "", - ) - .tr() - .toText10( - weight: FontWeight.w500, - color: AppColors.greyTextColor), - Utils.buildSvgWithAssets( - icon: AppAssets.lab_result_indicator, - width: 21, - height: 23, - iconColor: context.read().getColor( - recentLabResult.calculatedResultFlag ?? "", - )), ], ), ), - // SizedBox( - // width: 21, - // child: Utils.buildSvgWithAssets( - // icon: AppAssets.lab_result_indicator, - // width: 21, - // height: 23, - // iconColor: context.read().getColor( - // recentLabResult.calculatedResultFlag ?? "", - // ), - // ), - // ), ], - ) - + ), + Visibility( + visible: recentLabResult.referanceRange != null, + child: Text( + "Reference range: \n${recentLabResult.referanceRange!.trim()}".needTranslation, + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ), ], )); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 1652812..7b5341c 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -225,9 +225,9 @@ class _MedicalFilePageState extends State { ), AppCustomChipWidget( icon: AppAssets.blood_icon, - labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup.isEmpty}", + labelText: "Blood: ${appState.getUserBloodGroup.isEmpty ? "N/A" : appState.getUserBloodGroup.isEmpty}".needTranslation, iconColor: AppColors.primaryRedColor, - labelPadding: EdgeInsetsDirectional.only(end: 8.w), + labelPadding: EdgeInsetsDirectional.only(end: 4.w, start: 0.w), ), Consumer(builder: (context, insuranceVM, child) { return AppCustomChipWidget( @@ -390,6 +390,7 @@ class _MedicalFilePageState extends State { : ListView.separated( scrollDirection: Axis.horizontal, shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length, itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( @@ -440,9 +441,9 @@ class _MedicalFilePageState extends State { ), ); }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), - ).paddingSymmetrical(24.w, 0); + ); }), SizedBox(height: 24.h), "Lab & Radiology".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), @@ -695,11 +696,12 @@ class _MedicalFilePageState extends State { ), ).paddingSymmetrical(24.w, 0.h) : SizedBox( - height: 100.h, + height: 110.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: myAppointmentsVM.patientMyDoctorsList.length, shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( position: index, @@ -719,7 +721,8 @@ class _MedicalFilePageState extends State { fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: false, radius: 50.r), SizedBox(height: 8.h), - Expanded( + SizedBox( + width: 80.w, child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) .toString() .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) @@ -758,7 +761,7 @@ class _MedicalFilePageState extends State { }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), - ).paddingSymmetrical(24.w, 0); + ); }), SizedBox(height: 24.h), "Others".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 037860f..1b09dd5 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -53,7 +53,7 @@ class MedicalFileAppointmentCard extends StatelessWidget { ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(height: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), width: 200.w, child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/medical_file/widgets/medical_file_card.dart b/lib/presentation/medical_file/widgets/medical_file_card.dart index 8c38363..6343e2a 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -29,7 +29,7 @@ class MedicalFileCard extends StatelessWidget { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: backgroundColor, borderRadius: 12.r, - hasShadow: true + hasShadow: false ), padding: EdgeInsets.all(12.w), child: Column( diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart new file mode 100644 index 0000000..21ad42f --- /dev/null +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -0,0 +1,1840 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_ios_voip_kit_karmm/flutter_ios_voip_kit.dart'; +import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_camera_device.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_cmd_channel.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_live_transcription_message_info.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_share_action.dart'; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; +import 'package:hmg_patient_app_new/core/utils/jwt.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/video_view.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:image_picker/image_picker.dart'; + +class CallScreen extends StatefulHookWidget { + const CallScreen({Key? key}) : super(key: key); + + @override + State createState() => _CallScreenState(); +} + +class _CallScreenState extends State { + static TextEditingController changeNameController = TextEditingController(); + double opacityLevel = 1.0; + + final voIPKit = FlutterIOSVoIPKit.instance; + + bool isScreenBeingShared = false; + + void _changeOpacity() { + setState(() => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0); + } + + @override + Widget build(BuildContext context) { + var zoom = ZoomVideoSdk(); + var eventListener = ZoomVideoSdkEventListener(); + var isInSession = useState(false); + var sessionName = useState(''); + var sessionPassword = useState(''); + var users = useState([]); + var fullScreenUser = useState(null); + var sharingUser = useState(null); + var videoInfo = useState(''); + var isSharing = useState(false); + var isMuted = useState(true); + var isVideoOn = useState(false); + var isSpeakerOn = useState(false); + var isRenameModalVisible = useState(false); + var isRecordingStarted = useState(false); + var isMicOriginalOn = useState(false); + var isMounted = useIsMounted(); + var audioStatusFlag = useState(false); + var videoStatusFlag = useState(false); + var userNameFlag = useState(false); + var userShareStatusFlag = useState(false); + var isReceiveSpokenLanguageContentEnabled = useState(false); + var isVideoMirrored = useState(false); + var isOriginalAspectRatio = useState(false); + var isPiPView = useState(false); + var isSharedCamera = useState(false); + CameraShareView cameraShareView = const CameraShareView(creationParams: {}); + + //hide status bar + SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); + var circleButtonSize = 65.0; + Color backgroundColor = const Color(0xFF232323); + Color buttonBackgroundColor = const Color.fromRGBO(0, 0, 0, 0.6); + Color chatTextColor = const Color(0xFFAAAAAA); + Widget changeNamePopup; + final args = ModalRoute.of(context)!.settings.arguments as CallArguments; + + useEffect(() { + Future.microtask(() async { + var token = generateJwt(args.sessionName, args.role); + + Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); + + try { + Map SDKaudioOptions = {"connect": true, "mute": false, "autoAdjustSpeakerVolume": false}; + Map SDKvideoOptions = { + "localVideoOn": true, + }; + JoinSessionConfig joinSession = JoinSessionConfig( + sessionName: args.sessionName, + sessionPassword: args.sessionPwd, + token: token, + userName: args.displayName, + audioOptions: SDKaudioOptions, + videoOptions: SDKvideoOptions, + sessionIdleTimeoutMins: int.parse(args.sessionIdleTimeoutMins), + ); + await zoom.joinSession(joinSession); + } catch (e) { + const AlertDialog( + title: Text("Error"), + content: Text("Failed to join the session"), + ); + Future.delayed(const Duration(milliseconds: 1000)).asStream().listen((event) { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + } + }); + return null; + }, []); + + useEffect(() { + final sessionJoinListener = eventListener.addListener(EventType.onSessionJoin, (data) async { + data = data as Map; + isInSession.value = true; + zoom.session.getSessionName().then((value) => sessionName.value = value!); + sessionPassword.value = await zoom.session.getSessionPassword(); + debugPrint("sessionPhonePasscode: ${await zoom.session.getSessionPhonePasscode()}"); + ZoomVideoSdkUser mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(data['sessionUser'])); + List? remoteUsers = await zoom.session.getRemoteUsers(); + var muted = await mySelf.audioStatus?.isMuted(); + var videoOn = await mySelf.videoStatus?.isOn(); + var speakerOn = await zoom.audioHelper.getSpeakerStatus(); + fullScreenUser.value = mySelf; + remoteUsers?.insert(0, mySelf); + isMuted.value = muted!; + isSpeakerOn.value = speakerOn; + isVideoOn.value = videoOn!; + users.value = remoteUsers!; + isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(); + }); + + final sessionLeaveListener = eventListener.addListener(EventType.onSessionLeave, (data) async { + data = data as Map; + debugPrint("onSessionLeave: ${data['reason']}"); + isInSession.value = false; + users.value = []; + fullScreenUser.value = null; + // Navigator.popAndPushNamed( + // context, + // "Join", + // arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + // ); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }); + + final sessionNeedPasswordListener = eventListener.addListener(EventType.onSessionNeedPassword, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Need Password'), + content: const Text('Password is required'), + actions: [ + TextButton( + onPressed: () async => { + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final sessionPasswordWrongListener = eventListener.addListener(EventType.onSessionPasswordWrong, (data) async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Session Password Incorrect'), + content: const Text('Password is wrong'), + actions: [ + TextButton( + onPressed: () async => { + Navigator.popAndPushNamed(context, 'Join', arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)), + await zoom.leaveSession(false), + }, + child: const Text('OK'), + ), + ], + ), + ); + }); + + final userVideoStatusChangedListener = eventListener.addListener(EventType.onUserVideoStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.videoStatus?.isOn().then((on) => isVideoOn.value = on); + } + } + } + videoStatusFlag.value = !videoStatusFlag.value; + }); + + final userAudioStatusChangedListener = eventListener.addListener(EventType.onUserAudioStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['changedUsers']) as List; + List userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + for (var user in userList) { + { + if (user.userId == mySelf?.userId) { + mySelf?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + } + } + } + audioStatusFlag.value = !audioStatusFlag.value; + }); + + final userShareStatusChangeListener = eventListener.addListener(EventType.onUserShareStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString())); + ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction'])); + + if (shareAction.shareStatus == ShareStatus.Start || shareAction.shareStatus == ShareStatus.Resume) { + sharingUser.value = shareUser; + fullScreenUser.value = shareUser; + isSharing.value = (shareUser.userId == mySelf?.userId); + + setState(() { + isScreenBeingShared = true; + }); + Future.delayed(const Duration(seconds: 2), () { + setState(() { + isScreenBeingShared = false; + }); + }); + } else { + sharingUser.value = null; + isSharing.value = false; + isSharedCamera.value = false; + fullScreenUser.value = mySelf; + isScreenBeingShared = false; + setState(() { + isScreenBeingShared = true; + }); + Future.delayed(const Duration(seconds: 2), () { + setState(() { + isScreenBeingShared = false; + }); + }); + } + userShareStatusFlag.value = !userShareStatusFlag.value; + }); + + final shareContentChangedListener = eventListener.addListener(EventType.onShareContentChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString())); + ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction'])); + if (shareAction.shareType == ShareType.Camera) { + debugPrint("Camera share started"); + isSharedCamera.value = (shareUser.userId == mySelf?.userId); + } + }); + + final userJoinListener = eventListener.addListener(EventType.onUserJoin, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + var userListJson = jsonDecode(data['remoteUsers']) as List; + List remoteUserList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + remoteUserList.insert(0, mySelf!); + users.value = remoteUserList; + }); + + final userLeaveListener = eventListener.addListener(EventType.onUserLeave, (data) async { + if (!isMounted()) return; + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + data = data as Map; + List? remoteUserList = await zoom.session.getRemoteUsers(); + var leftUserListJson = jsonDecode(data['leftUsers']) as List; + List leftUserLis = leftUserListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList(); + if (fullScreenUser.value != null) { + for (var user in leftUserLis) { + { + if (fullScreenUser.value?.userId == user.userId) { + fullScreenUser.value = mySelf; + } + } + } + } else { + fullScreenUser.value = mySelf; + } + remoteUserList?.add(mySelf!); + users.value = remoteUserList!; + }); + + final userNameChangedListener = eventListener.addListener(EventType.onUserNameChanged, (data) async { + if (!isMounted()) return; + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + int index; + for (var user in users.value) { + if (user.userId == changedUser.userId) { + index = users.value.indexOf(user); + users.value[index] = changedUser; + } + } + userNameFlag.value = !userNameFlag.value; + }); + + final commandReceived = eventListener.addListener(EventType.onCommandReceived, (data) async { + data = data as Map; + debugPrint("sender: ${ZoomVideoSdkUser.fromJson(jsonDecode(data['sender']))}, command: ${data['command']}"); + }); + + final liveStreamStatusChangeListener = eventListener.addListener(EventType.onLiveStreamStatusChanged, (data) async { + data = data as Map; + debugPrint("onLiveStreamStatusChanged: status: ${data['status']}"); + }); + + final liveTranscriptionStatusChangeListener = eventListener.addListener(EventType.onLiveTranscriptionStatus, (data) async { + data = data as Map; + debugPrint("onLiveTranscriptionStatus: status: ${data['status']}"); + }); + + final cloudRecordingStatusListener = eventListener.addListener(EventType.onCloudRecordingStatus, (data) async { + data = data as Map; + debugPrint("onCloudRecordingStatus: status: ${data['status']}"); + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (data['status'] == RecordingStatus.Start) { + if (mySelf != null && !mySelf.isHost!) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: const Text('The session is being recorded.'), + actions: [ + TextButton( + onPressed: () async { + await zoom.acceptRecordingConsent(); + if (context.mounted) { + Navigator.pop(context); + } + ; + }, + child: const Text('accept'), + ), + TextButton( + onPressed: () async { + String currentConsentType = await zoom.getRecordingConsentType(); + if (currentConsentType == ConsentType.ConsentType_Individual) { + await zoom.declineRecordingConsent(); + Navigator.pop(context); + } else { + await zoom.declineRecordingConsent(); + zoom.leaveSession(false); + if (!context.mounted) return; + Navigator.popAndPushNamed( + context, + "Join", + arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + ); + } + }, + child: const Text('decline'), + ), + ], + ), + ); + } + isRecordingStarted.value = true; + } else { + isRecordingStarted.value = false; + } + }); + + final liveTranscriptionMsgInfoReceivedListener = eventListener.addListener(EventType.onLiveTranscriptionMsgInfoReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onLiveTranscriptionMsgInfoReceived: content: ${messageInfo.messageContent}"); + }); + + final inviteByPhoneStatusListener = eventListener.addListener(EventType.onInviteByPhoneStatus, (data) async { + data = data as Map; + debugPrint("onInviteByPhoneStatus: status: ${data['status']}, reason: ${data['reason']}"); + }); + + final multiCameraStreamStatusChangedListener = eventListener.addListener(EventType.onMultiCameraStreamStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var status = data['status']; + for (var user in users.value) { + { + if (changedUser.userId == user.userId) { + if (status == MultiCameraStreamStatus.Joined) { + user.hasMultiCamera = true; + } else if (status == MultiCameraStreamStatus.Left) { + user.hasMultiCamera = false; + } + } + } + } + }); + + final requireSystemPermission = eventListener.addListener(EventType.onRequireSystemPermission, (data) async { + data = data as Map; + ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser'])); + var permissionType = data['permissionType']; + switch (permissionType) { + case SystemPermissionType.Camera: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Camera"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + case SystemPermissionType.Microphone: + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Can't Access Microphone"), + content: const Text("please turn on the toggle in system settings to grant permission"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + break; + } + }); + + final networkStatusChangeListener = eventListener.addListener(EventType.onUserVideoNetworkStatusChanged, (data) async { + data = data as Map; + ZoomVideoSdkUser? networkUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + + if (data['status'] == NetworkStatus.Bad) { + debugPrint("onUserVideoNetworkStatusChanged: status: ${data['status']}, user: ${networkUser.userName}"); + } + }); + + final eventErrorListener = eventListener.addListener(EventType.onError, (data) async { + data = data as Map; + String errorType = data['errorType']; + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: Text(errorType), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + if (errorType == Errors.SessionJoinFailed || errorType == Errors.SessionDisconnecting) { + Timer( + const Duration(milliseconds: 1000), + () => { + Navigator.popAndPushNamed( + context, + "Join", + arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role), + ), + }); + } + }); + + final userRecordingConsentListener = eventListener.addListener(EventType.onUserRecordingConsent, (data) async { + data = data as Map; + ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + debugPrint('userRecordingConsentListener: user= ${user.userName}'); + }); + + final callCRCDeviceStatusListener = eventListener.addListener(EventType.onCallCRCDeviceStatusChanged, (data) async { + data = data as Map; + debugPrint('onCallCRCDeviceStatusChanged: status = ${data['status']}'); + }); + + final originalLanguageMsgReceivedListener = eventListener.addListener(EventType.onOriginalLanguageMsgReceived, (data) async { + data = data as Map; + ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo'])); + debugPrint("onOriginalLanguageMsgReceived: content: ${messageInfo.messageContent}"); + }); + + final chatPrivilegeChangedListener = eventListener.addListener(EventType.onChatPrivilegeChanged, (data) async { + data = data as Map; + String type = data['privilege']; + debugPrint('chatPrivilegeChangedListener: type= $type'); + }); + + final testMicStatusListener = eventListener.addListener(EventType.onTestMicStatusChanged, (data) async { + data = data as Map; + String status = data['status']; + debugPrint('testMicStatusListener: status= $status'); + }); + + final micSpeakerVolumeChangedListener = eventListener.addListener(EventType.onMicSpeakerVolumeChanged, (data) async { + data = data as Map; + int type = data['micVolume']; + debugPrint('onMicSpeakerVolumeChanged: micVolume= $type, speakerVolume'); + }); + + final cameraControlRequestResultListener = eventListener.addListener(EventType.onCameraControlRequestResult, (data) async { + data = data as Map; + bool approved = data['approved']; + debugPrint('onCameraControlRequestResult: approved= $approved'); + }); + + final callOutUserJoinListener = eventListener.addListener(EventType.onCalloutJoinSuccess, (data) async { + data = data as Map; + String phoneNumber = data['phoneNumber']; + ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'])); + debugPrint('onCalloutJoinSuccess: phoneNumber= $phoneNumber, user= ${user.userName}'); + }); + + return () => { + sessionJoinListener.cancel(), + sessionLeaveListener.cancel(), + sessionPasswordWrongListener.cancel(), + sessionNeedPasswordListener.cancel(), + userVideoStatusChangedListener.cancel(), + userAudioStatusChangedListener.cancel(), + userJoinListener.cancel(), + userLeaveListener.cancel(), + userNameChangedListener.cancel(), + userShareStatusChangeListener.cancel(), + liveStreamStatusChangeListener.cancel(), + cloudRecordingStatusListener.cancel(), + inviteByPhoneStatusListener.cancel(), + eventErrorListener.cancel(), + commandReceived.cancel(), + liveTranscriptionStatusChangeListener.cancel(), + liveTranscriptionMsgInfoReceivedListener.cancel(), + multiCameraStreamStatusChangedListener.cancel(), + requireSystemPermission.cancel(), + userRecordingConsentListener.cancel(), + networkStatusChangeListener.cancel(), + callCRCDeviceStatusListener.cancel(), + originalLanguageMsgReceivedListener.cancel(), + chatPrivilegeChangedListener.cancel(), + testMicStatusListener.cancel(), + micSpeakerVolumeChangedListener.cancel(), + cameraControlRequestResultListener.cancel(), + callOutUserJoinListener.cancel(), + shareContentChangedListener.cancel(), + }; + }, [zoom, users.value, isMounted]); + + void selectVirtualBackgroundItem() async { + final ImagePicker picker = ImagePicker(); + // Pick an image. + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + await zoom.virtualBackgroundHelper.addVirtualBackgroundItem(image!.path); + } + + void onPressAudio() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final audioStatus = mySelf.audioStatus; + if (audioStatus != null) { + var muted = await audioStatus.isMuted(); + if (muted) { + await zoom.audioHelper.unMuteAudio(mySelf.userId); + } else { + await zoom.audioHelper.muteAudio(mySelf.userId); + } + } + } + } + + void onPressVideo() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + if (mySelf != null) { + final videoStatus = mySelf.videoStatus; + if (videoStatus != null) { + var videoOn = await videoStatus.isOn(); + if (videoOn) { + await zoom.videoHelper.stopVideo(); + } else { + await zoom.videoHelper.startVideo(); + } + } + } + } + + void onPressShare() async { + var isOtherSharing = await zoom.shareHelper.isOtherSharing(); + var isShareLocked = await zoom.shareHelper.isShareLocked(); + String? shareCameraViewResult = Errors.InternalError; + + if (isOtherSharing) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Other is sharing'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isShareLocked) { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text("Error"), + content: const Text('Share is locked by host'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ); + } else if (isSharing.value) { + zoom.shareHelper.stopShare(); + } else { + List options = [ + ListTile( + title: Text( + 'Share Device Screen', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + zoom.shareHelper.shareScreen(), + Navigator.of(context).pop(), + }, + ), + ListTile( + title: Text( + 'Share Camera', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + shareCameraViewResult = await zoom.shareHelper.startShareCamera(cameraShareView), + debugPrint('start camera: $shareCameraViewResult'), + Navigator.of(context).pop(), + }, + ), + ]; + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 60, + child: Scrollbar( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ), + ), + ); + }); + } + } + + void onSelectedUserVolume(ZoomVideoSdkUser user) async { + var isShareAudio = user.isSharing; + bool canSetVolume = await user.canSetUserVolume(user.userId, isShareAudio); + num userVolume; + + List options = [ + ListTile( + title: Text( + 'Adjust Volume', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + ListTile( + title: Text( + 'Current volume', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + debugPrint('user volume'), + userVolume = await user.getUserVolume(user.userId, isShareAudio), + debugPrint('user ${user.userName}\'s volume is ${userVolume!}'), + }, + ), + ]; + if (canSetVolume) { + options.add( + ListTile( + title: Text( + 'Volume up', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume < 10) + { + await user.setUserVolume(user.userId, userVolume + 1, isShareAudio), + } + else + { + debugPrint("Cannot volume up."), + } + }, + ), + ); + options.add( + ListTile( + title: Text( + 'Volume down', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + userVolume = await user.getUserVolume(user.userId, isShareAudio), + if (userVolume > 0) + { + await user.setUserVolume(user.userId, userVolume - 1, isShareAudio), + } + else + { + debugPrint("Cannot volume down."), + } + }, + ), + ); + } + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 58, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ], + ), + )); + }); + } + + void onPressCameraList() async { + List options = []; + List cameraList = await zoom.videoHelper.getCameraList(); + for (var camera in cameraList) { + options.add( + ListTile( + title: Text( + camera.deviceName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + await zoom.videoHelper.switchCamera(camera.deviceId), + Navigator.of(context).pop(), + }, + ), + ); + } + options.add( + ListTile( + title: Text( + "Cancel", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + ), + ), + onTap: () async => { + Navigator.of(context).pop(), + }, + ), + ); + showDialog( + context: context, + builder: (context) { + return Dialog( + elevation: 0.0, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: SizedBox( + height: options.length * 60, + child: Scrollbar( + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: ListTile.divideTiles( + context: context, + tiles: options, + ).toList(), + ), + ), + ), + ); + }); + } + + // Future onPressMore() async { + // ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + // bool isShareLocked = await zoom.shareHelper.isShareLocked(); + // bool canSwitchSpeaker = await zoom.audioHelper.canSwitchSpeaker(); + // bool canStartRecording = (await zoom.recordingHelper.canStartRecording()) == Errors.Success; + // debugPrint("canStartRecording ${await zoom.recordingHelper.canStartRecording()}"); + // var startLiveTranscription = (await zoom.liveTranscriptionHelper.getLiveTranscriptionStatus()) == LiveTranscriptionStatus.Start; + // bool canStartLiveTranscription = await zoom.liveTranscriptionHelper.canStartLiveTranscription(); + // bool isHost = (mySelf != null) ? (await mySelf.getIsHost()) : false; + // isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(); + // bool canCallOutToCRC = await zoom.CRCHelper.isCRCEnabled(); + // bool supportVB = await zoom.virtualBackgroundHelper.isSupportVirtualBackground(); + // List? shareActionList = await mySelf?.getShareActionList(); + // + // List options = [ + // ListTile( + // title: Text( + // 'More', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.w600, + // color: Colors.black, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Get Chat Privilege', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("Chat Privilege = ${await zoom.chatHelper.getChatPrivilege()}"), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // 'Get Session Dial-in Number infos', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("session number = ${await zoom.session.getSessionNumber()}"), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // 'Get Camera List', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("camera list= ${await zoom.videoHelper.getCameraList()}"), + // onPressCameraList(), + // Navigator.of(context).pop(), + // }, + // ), + // ListTile( + // title: Text( + // '${isMicOriginalOn.value ? 'Disable' : 'Enable'} Original Sound', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("${isMicOriginalOn.value}"), + // await zoom.audioSettingHelper.enableMicOriginalInput(!isMicOriginalOn.value), + // isMicOriginalOn.value = await zoom.audioSettingHelper.isMicOriginalInputEnable(), + // debugPrint("Original sound ${isMicOriginalOn.value ? 'Enabled' : 'Disabled'}"), + // Navigator.of(context).pop(), + // }, + // ) + // ]; + // + // if (supportVB) { + // options.add( + // ListTile( + // title: Text( + // 'Add Virtual Background', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // selectVirtualBackgroundItem(), + // Navigator.of(context).pop(), + // }, + // ), + // ); + // } + // + // if (canCallOutToCRC) { + // options.add(ListTile( + // title: Text( + // 'Call-out to CRC devices', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint('CRC result = ${await zoom.CRCHelper.callCRCDevice("bjn.vc", ZoomVideoSdkCRCProtocolType.SIP)}'), + // Navigator.of(context).pop(), + // }, + // )); + // options.add(ListTile( + // title: Text( + // 'Cancel call-out to CRC devices', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint('cancel result= ${await zoom.CRCHelper.cancelCallCRCDevice()}'), + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (canSwitchSpeaker) { + // options.add(ListTile( + // title: Text( + // 'Turn ${isSpeakerOn.value ? 'off' : 'on'} Speaker', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.audioHelper.setSpeaker(!isSpeakerOn.value), + // isSpeakerOn.value = await zoom.audioHelper.getSpeakerStatus(), + // debugPrint('Turned ${isSpeakerOn.value ? 'on' : 'off'} Speaker'), + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (isHost) { + // options.add(ListTile( + // title: Text( + // '${isShareLocked ? 'Unlock' : 'Lock'} Share', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // debugPrint("isShareLocked = ${await zoom.shareHelper.lockShare(!isShareLocked)}"), + // Navigator.of(context).pop(), + // })); + // options.add(ListTile( + // title: Text( + // 'Change Name', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () => { + // isRenameModalVisible.value = true, + // Navigator.of(context).pop(), + // }, + // )); + // } + // + // if (canStartLiveTranscription) { + // options.add(ListTile( + // title: Text( + // "${startLiveTranscription ? 'Stop' : 'Start'} Live Transcription", + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (startLiveTranscription) + // { + // debugPrint('stopLiveTranscription= ${await zoom.liveTranscriptionHelper.stopLiveTranscription()}'), + // } + // else + // { + // debugPrint('startLiveTranscription= ${await zoom.liveTranscriptionHelper.startLiveTranscription()}'), + // }, + // Navigator.of(context).pop(), + // }, + // )); + // options.add(ListTile( + // title: Text( + // '${isReceiveSpokenLanguageContentEnabled.value ? 'Disable' : 'Enable'} receiving original caption', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.liveTranscriptionHelper.enableReceiveSpokenLanguageContent(!isReceiveSpokenLanguageContentEnabled.value), + // isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled(), + // debugPrint("isReceiveSpokenLanguageContentEnabled = ${isReceiveSpokenLanguageContentEnabled.value}"), + // Navigator.of(context).pop(), + // })); + // } + // + // if (canStartRecording) { + // options.add(ListTile( + // title: Text( + // '${isRecordingStarted.value ? 'Stop' : 'Start'} Recording', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (!isRecordingStarted.value) + // { + // debugPrint('isRecordingStarted = ${await zoom.recordingHelper.startCloudRecording()}'), + // } + // else + // { + // debugPrint('isRecordingStarted = ${await zoom.recordingHelper.stopCloudRecording()}'), + // }, + // Navigator.of(context).pop(), + // })); + // } + // + // if (Platform.isAndroid) { + // bool isFlashlightSupported = await zoom.videoHelper.isSupportFlashlight(); + // bool isFlashlightOn = await zoom.videoHelper.isFlashlightOn(); + // if (isFlashlightSupported) { + // options.add(ListTile( + // title: Text( + // '${isFlashlightOn ? 'Turn Off' : 'Turn On'} Flashlight', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // if (!isFlashlightOn) + // { + // await zoom.videoHelper.turnOnOrOffFlashlight(true), + // } + // else + // { + // await zoom.videoHelper.turnOnOrOffFlashlight(false), + // }, + // Navigator.of(context).pop(), + // })); + // } + // } + // + // if (Platform.isIOS) { + // options.add(ListTile( + // title: Text( + // '${isPiPView.value ? 'Disable' : 'Enable'} picture in picture view', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // isPiPView.value = !isPiPView.value, + // Navigator.of(context).pop(), + // })); + // } + // + // if (isVideoOn.value) { + // options.add(ListTile( + // title: Text( + // 'Mirror the video', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.videoHelper.mirrorMyVideo(!isVideoMirrored.value), + // isVideoMirrored.value = await zoom.videoHelper.isMyVideoMirrored(), + // Navigator.of(context).pop(), + // })); + // options.add(ListTile( + // title: Text( + // '${isOriginalAspectRatio.value ? 'Enable' : 'Disable'} original aspect ratio', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.normal, + // color: Colors.black, + // ), + // ), + // ), + // onTap: () async => { + // await zoom.videoHelper.enableOriginalAspectRatio(!isOriginalAspectRatio.value), + // isOriginalAspectRatio.value = await zoom.videoHelper.isOriginalAspectRatioEnabled(), + // debugPrint("isOriginalAspectRatio= ${isOriginalAspectRatio.value}"), + // Navigator.of(context).pop(), + // })); + // } + // + // showDialog( + // context: context, + // builder: (context) { + // return Dialog( + // elevation: 0.0, + // insetPadding: const EdgeInsets.symmetric(horizontal: 40), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + // child: SizedBox( + // height: 500, + // child: Scrollbar( + // child: ListView( + // shrinkWrap: true, + // scrollDirection: Axis.vertical, + // children: ListTile.divideTiles( + // context: context, + // tiles: options, + // ).toList(), + // ), + // ), + // ), + // ); + // }); + // } + + void onLeaveSession(bool isEndSession) async { + await zoom.leaveSession(isEndSession); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + } + + void showLeaveOptions() async { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + bool isHost = await mySelf!.getIsHost(); + + Widget endSession; + Widget leaveSession; + Widget cancel = TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); //close Dialog + }, + ); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + endSession = TextButton( + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = TextButton( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + default: + endSession = CupertinoActionSheetAction( + isDestructiveAction: true, + child: const Text('End Session'), + onPressed: () => onLeaveSession(true), + ); + leaveSession = CupertinoActionSheetAction( + child: const Text('Leave Session'), + onPressed: () => onLeaveSession(false), + ); + break; + } + + List options = [ + leaveSession, + cancel, + ]; + + if (Platform.isAndroid) { + if (isHost) { + options.removeAt(1); + options.insert(0, endSession); + } + showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: const Text("Do you want to leave this session?"), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + actions: options, + ); + }); + } else { + options.removeAt(1); + if (isHost) { + options.insert(1, endSession); + } + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + message: const Text('Are you sure that you want to leave the session?'), + actions: options, + cancelButton: cancel, + ), + ); + } + } + + final chatMessageController = TextEditingController(); + + void sendChatMessage(String message) async { + await zoom.chatHelper.sendChatToAll(message); + ZoomVideoSdkUser? self = await zoom.session.getMySelf(); + ZoomVideoSdkCmdChannel cmdChannel = zoom.cmdChannel; + for (var user in users.value) { + if (user.userId != self?.userId) { + await zoom.cmdChannel.sendCommand(user.userId, message); + } + } + chatMessageController.clear(); + // send the chat as a command + } + + void onSelectedUser(ZoomVideoSdkUser user) async { + setState(() { + fullScreenUser.value = user; + }); + } + + changeNamePopup = Center( + child: Stack( + children: [ + Visibility( + visible: isRenameModalVisible.value, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.bottomLeft, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Colors.white, + ), + width: MediaQuery.of(context).size.width - 130, + height: MediaQuery.of(context).size.height * 0.2, + child: Center( + child: (Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, left: 20), + child: Text( + 'Change Name', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10, left: 20), + child: SizedBox( + width: MediaQuery.of(context).size.width - 230, + child: TextField( + onEditingComplete: () {}, + autofocus: true, + cursorColor: Colors.black, + controller: changeNameController, + decoration: InputDecoration( + isDense: true, + hintText: 'New name', + hintStyle: TextStyle( + fontSize: 14.0, + color: chatTextColor, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 15), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Apply', + style: const TextStyle( + fontSize: 16, + ), + ), + onTap: () async { + if (fullScreenUser.value != null) { + ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf(); + await zoom.userHelper.changeName((mySelf?.userId)!, changeNameController.text); + changeNameController.clear(); + } + isRenameModalVisible.value = false; + }, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 40), + child: InkWell( + child: Text( + 'Cancel', + style: const TextStyle( + fontSize: 16, + ), + ), + onTap: () async { + isRenameModalVisible.value = false; + }, + ), + ) + ], + ), + ) + ], + )), + ), + ), + ], + )), + ], + ), + ); + + Widget fullScreenView; + Widget smallView; + Widget cameraView; + + if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) { + fullScreenView = AnimatedOpacity( + opacity: opacityLevel, + duration: const Duration(seconds: 3), + child: VideoView( + user: users.value.length > 1 ? users.value[1] : users.value[0], + hasMultiCamera: false, + isPiPView: isPiPView.value, + sharing: sharingUser.value == null ? false : (sharingUser.value?.userId == fullScreenUser.value?.userId), + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: true, + resolution: VideoResolution.Resolution360, + ), + ); + + smallView = Container( + height: 110, + margin: const EdgeInsets.only(left: 20, right: 20), + alignment: Alignment.center, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: users.value.length, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () async { + onSelectedUser(users.value[index]); + }, + onDoubleTap: () async { + onSelectedUserVolume(users.value[index]); + }, + child: Center( + child: VideoView( + user: users.value[index], + hasMultiCamera: false, + isPiPView: false, + sharing: false, + preview: false, + focused: false, + multiCameraIndex: "0", + videoAspect: VideoAspect.Original, + fullScreen: false, + resolution: VideoResolution.Resolution180, + ), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(), + ), + ); + } else { + fullScreenView = Container( + color: Colors.black, + child: const Center( + child: Text( + "Connecting...", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + )); + smallView = Container( + height: 110, + color: Colors.transparent, + ); + } + + cameraView = Offstage( + offstage: !isSharedCamera.value, + child: AnimatedOpacity( + opacity: isSharedCamera.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: cameraShareView, + ), + ); + + _changeOpacity; + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: backgroundColor, + body: Stack( + children: [ + // isScreenBeingShared ? fullScreenView : SizedBox.shrink(), + isScreenBeingShared ? SizedBox.shrink() : fullScreenView, + cameraView, + Container( + padding: const EdgeInsets.only(top: 35), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 80, + width: 180, + margin: const EdgeInsets.only(top: 16, left: 8), + padding: const EdgeInsets.all(8), + alignment: Alignment.topLeft, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + // color: buttonBackgroundColor, + ), + // child: InkWell( + // onTap: () async { + // showDialog( + // context: context, + // builder: (context) { + // return Dialog( + // elevation: 0.0, + // insetPadding: const EdgeInsets.symmetric(horizontal: 40), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + // child: FractionallySizedBox( + // heightFactor: 0.2, + // widthFactor: 0.3, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // ListView( + // shrinkWrap: true, + // children: ListTile.divideTiles( + // context: context, + // tiles: [ + // ListTile( + // title: Text( + // 'Session Information', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.w600, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Session Name', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // sessionName.value, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Session Password', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // sessionPassword.value, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ListTile( + // title: Text( + // 'Participants', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // ), + // ), + // ), + // subtitle: Text( + // '${users.value.length}', + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 12, + // ), + // ), + // ), + // ), + // ], + // ).toList(), + // ), + // ], + // ), + // )); + // }); + // }, + // child: Stack( + // children: [ + // Column( + // children: [ + // const Padding(padding: EdgeInsets.symmetric(vertical: 4)), + // Align( + // alignment: Alignment.centerLeft, + // child: Text( + // sessionName.value, + // overflow: TextOverflow.ellipsis, + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w600, + // color: Colors.white, + // ), + // ), + // ), + // ), + // const Padding(padding: EdgeInsets.symmetric(vertical: 5)), + // Align( + // alignment: Alignment.centerLeft, + // child: Text( + // "Participants: ${users.value.length}", + // style: GoogleFonts.lato( + // textStyle: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w600, + // color: Colors.white, + // ), + // ), + // ), + // ) + // ], + // ), + // Container( + // alignment: Alignment.centerRight, + // child: Image.asset( + // "assets/icons/unlocked@2x.png", + // height: 22, + // )), + // ], + // ), + // ), + ), + TextButton( + onPressed: (showLeaveOptions), + child: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.only(top: 16, right: 8), + padding: const EdgeInsets.only(top: 5, bottom: 5, left: 16, right: 16), + height: 28, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + color: buttonBackgroundColor, + ), + child: const Text( + "LEAVE", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFFE02828), + ), + ), + )), + ], + ), + Align( + alignment: Alignment.centerRight, + child: FractionallySizedBox( + widthFactor: 0.2, + heightFactor: 0.6, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: onPressAudio, + icon: isMuted.value ? Image.asset("assets/images/png/zoom/unmute@2x.png") : Image.asset("assets/images/png/zoom/mute@2x.png"), + iconSize: circleButtonSize, + tooltip: isMuted.value == true ? "Unmute" : "Mute", + ), + IconButton( + onPressed: onPressShare, + icon: isSharing.value ? Image.asset("assets/images/png/zoom/share-off@2x.png") : Image.asset("assets/images/png/zoom/share-on@2x.png"), + iconSize: circleButtonSize, + ), + IconButton( + onPressed: onPressVideo, + iconSize: circleButtonSize, + icon: isVideoOn.value ? Image.asset("assets/images/png/zoom/video-off@2x.png") : Image.asset("assets/images/png/zoom/video-on@2x.png"), + ), + Column( + children: [ + IconButton( + onPressed: () async { + onPressCameraList(); + }, + icon: Icon(Icons.cameraswitch, color: Colors.white), + color: Colors.white, + iconSize: 40.h, + ), + "Switch Camera".toText12(color: AppColors.whiteColor, isCenter: true, isBold: true) + ], + ), + ], + ), + )), + Container( + margin: const EdgeInsets.only(left: 16, right: 16, bottom: 40, top: 10), + alignment: Alignment.bottomCenter, + child: SizedBox( + height: MediaQuery.of(context).viewInsets.bottom == 0 ? 65 : MediaQuery.of(context).viewInsets.bottom + 18, + child: TextField( + maxLines: 1, + textAlign: TextAlign.left, + style: TextStyle(color: chatTextColor), + cursorColor: chatTextColor, + textAlignVertical: TextAlignVertical.center, + controller: chatMessageController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(left: 16, top: 10, bottom: 10, right: 16), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(width: 1, color: chatTextColor), //<-- SEE HERE + ), + hintText: 'Type your message here...', + hintStyle: TextStyle( + fontSize: 14.0, + color: chatTextColor, + ), + ), + onSubmitted: (String str) { + sendChatMessage(str); + }, + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(bottom: 120), + child: smallView, + ), + // CommentList(zoom: zoom, eventListener: eventListener), + changeNamePopup, + ], + )), + ], + )); + } +} + +class CallArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionIdleTimeoutMins; + final String role; + final int callType; + + CallArguments(this.sessionName, this.sessionPwd, this.displayName, this.sessionIdleTimeoutMins, this.role, this.isJoin, this.callType); +} + +class JoinArguments { + final bool isJoin; + final String sessionName; + final String sessionPwd; + final String displayName; + final String sessionTimeout; + final String roleType; + + JoinArguments(this.isJoin, this.sessionName, this.sessionPwd, this.displayName, this.sessionTimeout, this.roleType); +} diff --git a/lib/presentation/tele_consultation/zoom/video_view.dart b/lib/presentation/tele_consultation/zoom/video_view.dart new file mode 100644 index 0000000..4c1e049 --- /dev/null +++ b/lib/presentation/tele_consultation/zoom/video_view.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as FlutterZoomView; +import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; + +const SHOW_TALKING_ICON_DURATION = 2000; + +class VideoView extends FlutterZoomView.ZoomView { + const VideoView({ + super.key, + required super.user, + required super.sharing, + required super.preview, + required super.focused, + required super.hasMultiCamera, + required super.multiCameraIndex, + required super.videoAspect, + required super.fullScreen, + required super.resolution, + required super.isPiPView, + }); + + @override + Widget build(BuildContext context) { + var isVideoOn = useState(false); + var isTalking = useState(false); + var isMuted = useState(false); + var isMounted = useIsMounted(); + var zoom = ZoomVideoSdk(); + var isSharing = useState(false); + user?.audioStatus?.isMuted().then((muted) => isMuted.value = muted); + + useEffect(() { + updateVideoStatus() { + if (user == null) return; + Future.microtask(() async { + if (isMounted()) { + isVideoOn.value = (await user!.videoStatus!.isOn()); + isSharing.value = sharing; + } + }); + } + + resetAudioStatus() { + isTalking.value = false; + isMuted.value = false; + } + + updateAudioStatus() async { + if (!isMounted()) return; + var talking = await user?.audioStatus?.isTalking(); + var muted = await user?.audioStatus?.isMuted(); + isMuted.value = muted!; + isTalking.value = talking!; + if (talking) { + Timer( + const Duration(milliseconds: SHOW_TALKING_ICON_DURATION), + () => { + if (isMounted()) + { + isTalking.value = false, + } + }); + } + } + + updateVideoStatus(); + return null; + }, [zoom, user]); + + ImageIcon audioStatusIcon; + if (isTalking.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/images/png/zoom/talking@2x.png"), + ); + } else if (isMuted.value) { + audioStatusIcon = const ImageIcon( + AssetImage("assets/images/png/zoom/muted@2x.png"), + ); + } + // Pass parameters to the platform side. + final Map creationParams = {}; + creationParams.putIfAbsent("userId", () => user?.userId); + creationParams.putIfAbsent("sharing", () => sharing); + creationParams.putIfAbsent("preview", () => preview); + creationParams.putIfAbsent("focused", () => focused); + creationParams.putIfAbsent("hasMultiCamera", () => hasMultiCamera); + if (videoAspect.isEmpty) { + creationParams.putIfAbsent("videoAspect", () => VideoAspect.PanAndScan); + } else { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + creationParams.putIfAbsent("fullScreen", () => fullScreen); + if (resolution.isNotEmpty) { + creationParams.putIfAbsent("videoAspect", () => videoAspect); + } + + if (fullScreen) { + if (sharing) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else if (isVideoOn.value) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ); + } else { + return Container( + margin: const EdgeInsets.symmetric(vertical: 0), + child: Container( + alignment: Alignment.center, + child: const Image( + image: AssetImage("assets/images/png/zoom/default-avatar.png"), + )), + ); + } + } else { + if (isVideoOn.value || sharing) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: FlutterZoomView.View(creationParams: creationParams), + ), + Container( + height: 110, + width: 110, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + Container( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + // height: 110, + // width: 110, + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/images/png/zoom/muted@2x.png") : const AssetImage("assets/images/png/zoom/talking@2x.png"), + fit: BoxFit.cover, + ), + ) + ], + ), + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: 110, + width: 110, + child: Stack( + children: [ + Container( + height: 110, + width: 110, + decoration: BoxDecoration( + color: const Color(0xff232323), + border: Border.all( + color: const Color(0xff666666), + width: 1, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + alignment: Alignment.center, + child: Container( + alignment: Alignment.center, + child: const Image( + height: 60, + width: 60, + image: AssetImage("assets/images/png/zoom/default-avatar.png"), + )), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 20, + width: 110, + decoration: const BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + Container( + alignment: Alignment.bottomLeft, + margin: const EdgeInsets.only(left: 5), + child: Text( + user!.userName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + Container( + alignment: Alignment.bottomRight, + margin: const EdgeInsets.only(right: 5, bottom: 5), + child: Image( + height: 12, + width: 12, + image: isMuted.value ? const AssetImage("assets/images/png/zoom/muted@2x.png") : const AssetImage("assets/images/png/zoom/talking@2x.png"), + fit: BoxFit.cover, + ), + ), + ], + ), + ); + } + } + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index b37ebfa..ed5c0db 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart' import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; class AppRoutes { @@ -19,6 +20,7 @@ class AppRoutes { static const String eReferralPage = '/erReferralPage'; static const String comprehensiveCheckupPage = '/comprehensiveCheckupPage'; static const String homeHealthCarePage = '/homeHealthCarePage'; + static const String zoomCallPage = '/zoomCallPage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -29,6 +31,7 @@ class AppRoutes { medicalFilePage: (context) => MedicalFilePage(), eReferralPage: (context) => NewReferralPage(), comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), - homeHealthCarePage: (context) => HhcProceduresPage() + homeHealthCarePage: (context) => HhcProceduresPage(), + zoomCallPage: (context) => CallScreen() }; } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index ba7cefd..b5b752b 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -4,8 +4,11 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_callkit_incoming/entities/call_event.dart'; +import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/onboarding_screen.dart'; import 'package:hmg_patient_app_new/presentation/onboarding/splash_animation_screen.dart'; @@ -19,6 +22,9 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_ // import 'package:hmg_patient_app_new/presentation/authantication/login.dart'; import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; +import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:lottie/lottie.dart'; @@ -38,6 +44,7 @@ class _SplashScreenState extends State { late AuthenticationViewModel authVm; Future initializeStuff() async { + listenerEvent(); Timer( Duration(milliseconds: 500), () async { @@ -47,22 +54,155 @@ class _SplashScreenState extends State { ); await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { + bool isAppOpenedFromCall = await GetIt.instance().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; + LocalNotification.init(onNotificationClick: (payload) {}); - if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + if (isAppOpenedFromCall) { + navigateToTeleConsult(); } else { - Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + if (await Utils.getBoolFromPrefs(CacheConst.firstLaunch)) { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: OnboardingScreen()))); + Navigator.of(context).pushReplacement(FadePage(page: OnboardingScreen())); + } else { + // Navigator.of(context).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.of(context).pushReplacement(FadePage(page: LandingNavigation())); + } } }); var zoom = ZoomVideoSdk(); InitConfig initConfig = InitConfig( domain: "zoom.us", - enableLog: true, + enableLog: false, ); zoom.initSdk(initConfig); } + navigateToTeleConsult() async { + String roomID = await Utils.getStringFromPrefs(CacheConst.zoomRoomID); + print('RoomID: $roomID'); + + // GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); + Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); + + Navigator.of(GetIt.instance().navigatorKey.currentContext!).pushReplacement(FadePage(page: SplashAnimationScreen(routeWidget: LandingNavigation()))); + Navigator.pushReplacementNamed( + // context, + GetIt.instance().navigatorKey.currentContext!, + AppRoutes.zoomCallPage, + // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + arguments: CallArguments(roomID, "123", "Patient", "40", "0", true, 1), + // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + ); + } + + Future listenerEvent() async { + print('Call Canceled : ------->'); + + try { + FlutterCallkitIncoming.onEvent.listen((event) async { + switch (event!.event) { + case Event.actionCallIncoming: + print('actionCallIncoming Event Received: ------->'); + break; + case Event.actionCallStart: + print('actionCallStart Event Received: ------->'); + break; + case Event.actionCallAccept: + print('actionCallAccept Event Received: ------->'); + FlutterCallkitIncoming.endAllCalls(); + try { + navigateToTeleConsult(); + // String roomID = await Utils.getStringFromPrefs(SharedPrefsConsts.ZOOM_ROOM_ID); + // String roomPass = await Utils.getStringFromPrefs(SharedPrefsConsts.ZOOM_ROOM_PASS); + + // print('RoomID: $roomID'); + // print('RoomPass: $roomPass'); + + // Future.delayed(const Duration(milliseconds: 3000), () async { + // // try { + // // String teleConsultURL = "https://teleconsult.meena-health.com/api/videocall/doVideoCall?roomid="; + // // await Utils.getStringFromPrefs(SharedPrefsConsts.PATIENT_MRN).then((value) { + // // teleConsultURL = teleConsultURL + value; + // // print("TeleConsult URL: $teleConsultURL"); + // // Uri uri = Uri.parse(teleConsultURL); + // // launchUrl(uri, mode: LaunchMode.externalApplication, webOnlyWindowName: "TeleConsult"); + // // }); + // + // // var cameraStatus = await Permission.camera.request(); + // // var micStatus = await Permission.microphone.request(); + // // var permissionStatus = await [Permission.microphone, Permission.camera, Permission.notification].request(); + // // if (permissionStatus[1]?.isDenied == true) { + // // await Permission.camera.request(); + // // } + // // if (permissionStatus[0]?.isDenied == true) { + // // await Permission.microphone.request(); + // // } + // + // Navigator.pushNamed( + // // context, + // navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments(roomID, roomPass, "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); + // // } catch (err) { + // // print(err); + // // } + // }); + // String teleConsultURL = "https://teleconsult.meena-health.com/api/videocall/doVideoCall?roomid="; + // await Utils.getStringFromPrefs(SharedPrefsConsts.PATIENT_MRN).then((value) { + // teleConsultURL = teleConsultURL + value; + // print("TeleConsult URL: $teleConsultURL"); + // Uri uri = Uri.parse(teleConsultURL); + // launchUrl(uri, mode: LaunchMode.inAppBrowserView, webOnlyWindowName: "Test"); + // }); + // navigatorKey.currentState!.pushNamed(AppRoutes.incomingCallScreenRoute); + // await Navigator.pushNamed(context, AppRoutes.incomingCallScreenRoute).then((value) {}); + } catch (e) { + print("ERRORRRR!!! ${e.toString()}"); + // await AppSharedPreferences().setObject('call_data', data); + // var data1 = await AppSharedPreferences().getObject('call_data'); + } + break; + case Event.actionCallDecline: + print('actionCallConnected Event Received: ------->'); + await FlutterCallkitIncoming.endAllCalls(); + GetIt.instance().remove(key: CacheConst.isAppOpenedFromCall); + break; + case Event.actionCallEnded: + break; + case Event.actionCallTimeout: + break; + case Event.actionCallCallback: + break; + case Event.actionCallToggleHold: + break; + case Event.actionCallToggleMute: + break; + case Event.actionCallToggleDmtf: + break; + case Event.actionCallToggleGroup: + break; + case Event.actionCallToggleAudioSession: + break; + case Event.actionDidUpdateDevicePushTokenVoip: + break; + case Event.actionCallCustom: + break; + case Event.actionCallConnected: + print('actionCallConnected Event Received: ------->'); + await FlutterCallkitIncoming.endAllCalls(); + break; + } + }); + } on Exception catch (e) { + print(e); + print("ERRORRRR!!! ${e.toString()}"); + } + } + /// load the Privilege from service Future loadPrivilege() async { // ProjectViewModel projectProvider = Provider.of(context, listen: false); diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 6904edc..8be16b7 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -22,6 +22,7 @@ class AppCustomChipWidget extends StatelessWidget { this.deleteIconColor = AppColors.textColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, + this.isIconPNG = false, this.onChipTap, this.labelPadding, this.onDeleteTap, @@ -42,6 +43,7 @@ class AppCustomChipWidget extends StatelessWidget { final OutlinedBorder? shape; final EdgeInsets? padding; final EdgeInsetsDirectional? labelPadding; + final bool isIconPNG; final void Function()? onChipTap; final void Function()? onDeleteTap; @@ -66,8 +68,10 @@ class AppCustomChipWidget extends StatelessWidget { child: icon.isNotEmpty ? Chip( avatar: icon.isNotEmpty - ? Utils.buildSvgWithAssets( - icon: icon, + ? isIconPNG + ? Image.asset(icon, width: iconS, height: iconS) + : Utils.buildSvgWithAssets( + icon: icon, width: iconS, height: iconS, iconColor: iconHasColor ? iconColor : null, diff --git a/pubspec.yaml b/pubspec.yaml index 3d6604c..6f014cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,8 @@ dependencies: geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.13.1 - flutter_zoom_videosdk: ^2.1.10 + flutter_zoom_videosdk: 2.1.10 + dart_jsonwebtoken: ^3.2.0 dartz: ^0.10.1 equatable: ^2.0.7 google_api_availability: ^5.0.1 @@ -79,6 +80,7 @@ dependencies: path_provider: ^2.0.8 open_filex: ^4.7.0 flutter_swiper_view: ^1.1.8 + flutter_callkit_incoming: ^3.0.0 location: ^8.0.1 gms_check: ^1.0.4 @@ -108,6 +110,7 @@ flutter: - assets/images/png/body_parts/female/ - assets/images/svg/ - assets/images/png/ + - assets/images/png/zoom/ - assets/animations/ - assets/animations/lottie/