From 514bf499563c5e456531d15bf99196478d29a0f0 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 19 Apr 2026 14:17:43 +0300 Subject: [PATCH 1/5] Call screen update --- .../tele_consultation/zoom/call_screen.dart | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index a2721aba..3e7720a1 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -21,11 +22,14 @@ 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/generated/locale_keys.g.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/livecare_permission_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'; +import 'package:permission_handler/permission_handler.dart'; class CallScreen extends StatefulHookWidget { const CallScreen({Key? key}) : super(key: key); @@ -607,32 +611,56 @@ class _CallScreenState extends State { } 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); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; + + if (cameraGranted && micGranted) { + 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); + } } } + } else { + await LiveCarePermissionService.instance.showOpenSettingsDialog( + context, + title: LocaleKeys.permissionsProfile.tr(), + message: LocaleKeys.liveCarePermissions.tr(), + ); } } 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(); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; + + if (cameraGranted && micGranted) { + 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(); + } } } + } else { + await LiveCarePermissionService.instance.showOpenSettingsDialog( + context, + title: LocaleKeys.permissionsProfile.tr(), + message: LocaleKeys.liveCarePermissions.tr(), + ); } } From e842d852547c030c03f6d85935d06f6d5870f621 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Sun, 19 Apr 2026 14:28:07 +0300 Subject: [PATCH 2/5] Fixes & Translation --- android/app/src/main/AndroidManifest.xml | 4 +- assets/langs/ar-SA.json | 5 +- assets/langs/en-US.json | 5 +- lib/core/api_consts.dart | 2 +- .../immediate_livecare_view_model.dart | 45 ++- lib/generated/locale_keys.g.dart | 1 + .../immediate_livecare_payment_details.dart | 4 +- ...mediate_livecare_pending_request_page.dart | 291 +++++++++--------- .../widgets/profile_picture_widget.dart | 29 +- lib/widgets/countdown_timer.dart | 78 +---- lib/widgets/image_picker.dart | 53 +++- pubspec.yaml | 2 + 12 files changed, 254 insertions(+), 265 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f5b408d8..762c964d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,8 +42,8 @@ tools:node="remove" /> - - + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index c25d80ef..8db00a5f 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1289,7 +1289,7 @@ "quickLinks": "روابط سريعة", "viewMedicalFileLandingPage": "عرض الملف الطبي", "immediateLiveCareRequest": "طلب لايف كير فوري", - "yourTurnIsAfterPatients": "دورك بعد {count} مريض. يرجى الانتظار حتى يحين دورك، وسيناديك الطبيب قريباً.", + "yourTurnIsAfterPatients": "أمامك {count} مريض. سيقوم الطبيب بالتواصل معكً.", "dontHaveHHCOrders": "ليس لديك أي أوامر رعاية صحية منزلية حتى الآن.", "hhcOrders": "أوامر الرعاية الصحية المنزلية", "requestedServices": "الخدمات المطلوبة", @@ -1825,5 +1825,6 @@ "rechargePageKey": "اختر المستفيد", "selectFamilyFile": "اختيار ", "ancillaryOrdersListNew": "طلبات غير مكتملة", - "medicalReportNew": "التقارير الطبية" + "medicalReportNew": "التقارير الطبية", + "livecarePendingReuqestNew": "انتظار الطبيب" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 3388e54f..362d86e4 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1279,7 +1279,7 @@ "quickLinks": "Quick Links", "viewMedicalFileLandingPage": "View medical file", "immediateLiveCareRequest": "Immediate LiveCare Request", - "yourTurnIsAfterPatients": "Your turn is after {count} patients. Please wait for your turn, The doctor will call you shortly.", + "yourTurnIsAfterPatients": "There are {count} patients ahead of you. The doctor will call you shortly.", "dontHaveHHCOrders": "You don't have any Home Health Care orders yet.", "hhcOrders": "HHC Orders", "requestedServices": "Requested Services", @@ -1815,7 +1815,8 @@ "rechargePageKey": "Recharge Wallet", "selectFamilyFile": "Select", "ancillaryOrdersListNew": "Ancillary Orders List", - "medicalReportNew": "Medical Report" + "medicalReportNew": "Medical Report", + "livecarePendingReuqestNew": "Live Care Pending Request" } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ff39fa70..ae4eb152 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; 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 diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index e61292d6..8a30f154 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -53,8 +53,8 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { late AppState _appState; Duration countdownDuration = Duration(minutes: 1, seconds: 0); - late ValueNotifier durationNotifier; - late Timer timer; + ValueNotifier durationNotifier = ValueNotifier(Duration(minutes: 1, seconds: 0)); + Timer? timer; bool isShowNotEligibleDialog = false; String notEligibleErrorMsg = ""; @@ -264,16 +264,20 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { patientLiveCareHistoryList = apiResponse.data!; if (patientLiveCareHistoryList.isNotEmpty) { if (patientLiveCareHistoryList[0].callStatus! < 4) { - countdownDuration = Duration(seconds: 0); patientHasPendingLiveCareRequest = true; countdownDuration = Duration(minutes: patientLiveCareHistoryList.first.watingtimeInteger!, seconds: 0); - durationNotifier = ValueNotifier(countdownDuration); - startTimer(); + durationNotifier.value = countdownDuration; + // Start timer if not already running + if (timer == null || !timer!.isActive) { + startTimer(); + } } else { patientHasPendingLiveCareRequest = false; + stopTimer(); // Stop timer if call status >= 4 } } else { patientHasPendingLiveCareRequest = false; + stopTimer(); // Stop timer if no history } notifyListeners(); if (onSuccess != null) { @@ -312,18 +316,35 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { } void startTimer() { - timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); - notifyListeners(); + // Only start timer if it's not already active + if (timer == null || !timer!.isActive) { + timer = Timer.periodic(const Duration(seconds: 1), (_) { + addTime(); + }); + } } void addTime() { - final seconds = durationNotifier.value.inSeconds - 1; - if (seconds < 0) { + final currentSeconds = durationNotifier.value.inSeconds; + + if (currentSeconds <= 0) { + // Timer has reached zero, stop it timer?.cancel(); - // Handle end of timer here - // showEndMessage(); + durationNotifier.value = Duration.zero; } else { - durationNotifier.value = Duration(seconds: seconds); + // Decrease by 1 second and continue + durationNotifier.value = Duration(seconds: currentSeconds - 1); } } + + void stopTimer() { + timer?.cancel(); + } + + @override + void dispose() { + timer?.cancel(); + durationNotifier.dispose(); + super.dispose(); + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 01b27328..05b0bd20 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1818,5 +1818,6 @@ abstract class LocaleKeys { static const selectFamilyFile = 'selectFamilyFile'; static const ancillaryOrdersListNew = 'ancillaryOrdersListNew'; static const medicalReportNew = 'medicalReportNew'; + static const livecarePendingReuqestNew = 'livecarePendingReuqestNew'; } 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 b3801113..4bb1a0fd 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -239,8 +239,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], ).paddingSymmetrical(24.h, 0.h), - (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") - // (true) + // (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") + (true) ? CustomButton( text: LocaleKeys.confirmLiveCare.tr(context: context), onPressed: () async { diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart index 23c0c07b..0d42f7e5 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart @@ -1,5 +1,4 @@ -import 'dart:async'; - +import 'dart:ui' as ui; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -33,24 +32,19 @@ class _ImmediateLiveCarePendingRequestPageState extends State durationNotifier = ValueNotifier(immediateLiveCareViewModel.countdownDuration); - // Timer? timer; - @override void initState() { super.initState(); - scheduleMicrotask(() { - // countdownDuration = Duration(minutes: immediateLiveCareViewModel.patientLiveCareHistoryList[0].watingtimeInteger!, seconds: 0); - // durationNotifier = ValueNotifier(immediateLiveCareViewModel.countdownDuration); - // startTimer(); - }); } @override void dispose() { - // timer?.cancel(); - // durationNotifier.dispose(); + // Cancel the timer when leaving the page + try { + immediateLiveCareViewModel.stopTimer(); + } catch (e) { + // Handle case where viewModel might not be initialized + } super.dispose(); } @@ -65,7 +59,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State addTime()); - // // setState(() {}); - // } - // - // void addTime() { - // final seconds = durationNotifier.value.inSeconds - 1; - // if (seconds < 0) { - // timer?.cancel(); - // // Handle end of timer here - // // showEndMessage(); - // } else { - // durationNotifier.value = Duration(seconds: seconds); - // } - // } - // - // Future _onWillPop() async { - // timer?.cancel(); - // Navigator.of(context).pop(); - // return true; - // } - // - // Widget buildTime(Duration duration) { - // String twoDigits(int n) => n.toString().padLeft(2, '0'); - // final hours = twoDigits(duration.inHours); - // final minutes = twoDigits(duration.inMinutes.remainder(60)); - // final seconds = twoDigits(duration.inSeconds.remainder(60)); - // - // return Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // buildTimeColumn(hours, "Hours".needTranslation), - // buildTimeColumn(minutes, "Mins".needTranslation), - // buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), - // ], - // ); - // } - // - // Widget buildTimeColumn(String time, String label, {bool isLast = false}) { - // return Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Row( - // children: [ - // buildDigit(time[0]), - // buildDigit(time[1]), - // if (!isLast) buildTimeSeparator(), - // ], - // ), - // buildLabel(label), - // ], - // ); - // } - // - // Widget buildDigit(String digit) { - // return Container( - // padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - // // margin: const EdgeInsets.symmetric(horizontal: 2), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(8), - // ), - // child: ClipRect( - // child: AnimatedSwitcher( - // duration: const Duration(milliseconds: 600), - // switchInCurve: Curves.easeOutExpo, - // switchOutCurve: Curves.easeInExpo, - // transitionBuilder: (Widget child, Animation animation) { - // return Stack( - // children: [ - // SlideTransition( - // position: Tween( - // begin: const Offset(0, -1), - // end: const Offset(0, 1), - // ).animate(CurvedAnimation( - // parent: animation, - // curve: Curves.easeOutCubic, - // )), - // child: FadeTransition( - // opacity: animation, - // child: child, - // ), - // ), - // SlideTransition( - // position: Tween( - // begin: const Offset(0, -1), - // end: const Offset(0, 0), - // ).animate(CurvedAnimation( - // parent: animation, - // curve: Curves.bounceIn, - // )), - // child: FadeTransition( - // opacity: animation, - // child: child, - // ), - // ), - // ], - // ); - // }, - // child: Text( - // digit, - // key: ValueKey(digit), - // style: TextStyle( - // fontWeight: FontWeight.bold, - // color: Colors.black, - // fontSize: 20.f, - // ), - // ), - // ), - // ), - // ); - // } - // - // Widget buildLabel(String label) { - // return label.toText14(isBold: true); - // } - // - // Widget buildTimeSeparator() { - // return const Padding( - // padding: EdgeInsets.symmetric(horizontal: 2.0), - // child: Text( - // ":", - // style: TextStyle( - // color: Colors.black, - // fontSize: 20, - // ), - // ), - // ); - // } +// void startTimer() { +// // timer = Timer.periodic(const Duration(seconds: 1), (_) => addTime()); +// // setState(() {}); +// } +// +// void addTime() { +// final seconds = durationNotifier.value.inSeconds - 1; +// if (seconds < 0) { +// timer?.cancel(); +// // Handle end of timer here +// // showEndMessage(); +// } else { +// durationNotifier.value = Duration(seconds: seconds); +// } +// } +// +// Future _onWillPop() async { +// timer?.cancel(); +// Navigator.of(context).pop(); +// return true; +// } +// +// Widget buildTime(Duration duration) { +// String twoDigits(int n) => n.toString().padLeft(2, '0'); +// final hours = twoDigits(duration.inHours); +// final minutes = twoDigits(duration.inMinutes.remainder(60)); +// final seconds = twoDigits(duration.inSeconds.remainder(60)); +// +// return Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// buildTimeColumn(hours, "Hours".needTranslation), +// buildTimeColumn(minutes, "Mins".needTranslation), +// buildTimeColumn(seconds, "Secs".needTranslation, isLast: true), +// ], +// ); +// } +// +// Widget buildTimeColumn(String time, String label, {bool isLast = false}) { +// return Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Row( +// children: [ +// buildDigit(time[0]), +// buildDigit(time[1]), +// if (!isLast) buildTimeSeparator(), +// ], +// ), +// buildLabel(label), +// ], +// ); +// } +// +// Widget buildDigit(String digit) { +// return Container( +// padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), +// // margin: const EdgeInsets.symmetric(horizontal: 2), +// decoration: BoxDecoration( +// color: Colors.white, +// borderRadius: BorderRadius.circular(8), +// ), +// child: ClipRect( +// child: AnimatedSwitcher( +// duration: const Duration(milliseconds: 600), +// switchInCurve: Curves.easeOutExpo, +// switchOutCurve: Curves.easeInExpo, +// transitionBuilder: (Widget child, Animation animation) { +// return Stack( +// children: [ +// SlideTransition( +// position: Tween( +// begin: const Offset(0, -1), +// end: const Offset(0, 1), +// ).animate(CurvedAnimation( +// parent: animation, +// curve: Curves.easeOutCubic, +// )), +// child: FadeTransition( +// opacity: animation, +// child: child, +// ), +// ), +// SlideTransition( +// position: Tween( +// begin: const Offset(0, -1), +// end: const Offset(0, 0), +// ).animate(CurvedAnimation( +// parent: animation, +// curve: Curves.bounceIn, +// )), +// child: FadeTransition( +// opacity: animation, +// child: child, +// ), +// ), +// ], +// ); +// }, +// child: Text( +// digit, +// key: ValueKey(digit), +// style: TextStyle( +// fontWeight: FontWeight.bold, +// color: Colors.black, +// fontSize: 20.f, +// ), +// ), +// ), +// ), +// ); +// } +// +// Widget buildLabel(String label) { +// return label.toText14(isBold: true); +// } +// +// Widget buildTimeSeparator() { +// return const Padding( +// padding: EdgeInsets.symmetric(horizontal: 2.0), +// child: Text( +// ":", +// style: TextStyle( +// color: Colors.black, +// fontSize: 20, +// ), +// ), +// ); +// } } diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index e7abcb12..6b30c0fc 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -135,13 +135,13 @@ class _ProfilePictureWidgetState extends State { print('✅ Profile image loaded successfully for user: $currentPatientId'); if (mounted) { _tryCacheExistingImage(); - setState(() {}); // Force rebuild to show new data + setState(() {}); // Force rebuild to show new data } }, onError: (error) { print('❌ Error loading profile image: $error'); if (mounted) { - setState(() {}); // Force rebuild to show default avatar + setState(() {}); // Force rebuild to show default avatar } }, ); @@ -365,16 +365,16 @@ class _ProfilePictureWidgetState extends State { try { print('=== Checking gallery permission ==='); - // Determine which permission to check based on platform and Android version - Permission galleryPermission; - - if (Platform.isIOS) { - galleryPermission = Permission.photos; - } else { - // Android: use photos permission which handles API level differences automatically - galleryPermission = Permission.photos; + // For Android 13+ (API 33+), the Android Photo Picker handles permissions internally + // No need to request READ_MEDIA_IMAGES or READ_EXTERNAL_STORAGE permissions + if (Platform.isAndroid) { + print('✅ Android Photo Picker will handle permissions internally'); + return true; } + // iOS: Check photos permission + Permission galleryPermission = Permission.photos; + // First check current status PermissionStatus currentStatus = await galleryPermission.status; print('Current gallery permission status: $currentStatus'); @@ -520,9 +520,7 @@ class _ProfilePictureWidgetState extends State { // Use cached decoded bytes — update cache if source data changed final String? imageData = GetIt.instance().getProfileImageData; - final String? currentHash = (imageData != null && imageData.isNotEmpty) - ? '${imageData.length}_${imageData.hashCode}' - : null; + final String? currentHash = (imageData != null && imageData.isNotEmpty) ? '${imageData.length}_${imageData.hashCode}' : null; // Re-decode only if the underlying data actually changed if (currentHash != null && currentHash != _cachedImageDataHash) { @@ -574,9 +572,7 @@ class _ProfilePictureWidgetState extends State { builder: (context, profileVm, _) { // If we already have cached bytes, show the image even while "loading" // to prevent the shimmer→image blink on page open - final bool showShimmer = profileVm.isProfileImageLoading && - _cachedImageBytes == null && - _selectedImage == null; + final bool showShimmer = profileVm.isProfileImageLoading && _cachedImageBytes == null && _selectedImage == null; return Center( child: Stack( @@ -644,4 +640,3 @@ class _ProfilePictureWidgetState extends State { ); } } - diff --git a/lib/widgets/countdown_timer.dart b/lib/widgets/countdown_timer.dart index f809d9f5..e51f9291 100644 --- a/lib/widgets/countdown_timer.dart +++ b/lib/widgets/countdown_timer.dart @@ -23,93 +23,42 @@ Widget buildTime(Duration duration, {bool isHomePage = false}) { Widget buildTimeColumn(String time, String label, {bool isLast = false}) { return Column( - children: [ Row( - children: [ Column( children: [ Row( children: [ buildDigit(time[0]), + SizedBox(width: 2.w), buildDigit(time[1]), ], ), + SizedBox(height: 4.h), buildLabel(label), ], ), if (!isLast) ...[ - SizedBox(width: 8.w,), + SizedBox(width: 8.w), buildTimeSeparator(), - SizedBox(width: 8.w,), + SizedBox(width: 8.w), ], ], ), - ], ); } Widget buildDigit(String digit) { - return - // Container( - // padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - // // margin: const EdgeInsets.symmetric(horizontal: 2), - // decoration: BoxDecoration( - // color: AppColors.whiteColor, - // borderRadius: BorderRadius.circular(8), - // ), - // child: - ClipRect( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 600), - switchInCurve: Curves.easeOutExpo, - switchOutCurve: Curves.easeInExpo, - transitionBuilder: (Widget child, Animation animation) { - return Stack( - children: [ - SlideTransition( - position: Tween( - begin: const Offset(0, -1), - end: const Offset(0, 1), - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - )), - child: FadeTransition( - opacity: animation, - child: child, - ), - ), - SlideTransition( - position: Tween( - begin: const Offset(0, -1), - end: const Offset(0, 0), - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.bounceIn, - )), - child: FadeTransition( - opacity: animation, - child: child, - ), - ), - ], - ); - }, - child: Text( - digit, - key: ValueKey(digit), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black, - fontSize: 27.f, - ), - ), - ), - ); - // ); + return Text( + digit, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + fontSize: 27.f, + ), + ); } Widget buildLabel(String label) { @@ -118,12 +67,13 @@ Widget buildLabel(String label) { Widget buildTimeSeparator() { return Padding( - padding: EdgeInsets.symmetric(horizontal: 2.0), + padding: EdgeInsets.only(bottom: 16.h), child: Text( ":", style: TextStyle( color: Colors.black, fontSize: 28.f, + fontWeight: FontWeight.bold, ), ), ); diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index f7665a56..3fa7d061 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -10,8 +10,11 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:image_picker_android/image_picker_android.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; final ImagePicker picker = ImagePicker(); +final ImagePickerPlatform _pickerPlatform = ImagePickerPlatform.instance; class ImageOptions { static void showImageOptionsNew( @@ -214,25 +217,47 @@ class ImageOptions { } void galleryImageAndroid(Function(String, File) image) async { - XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); - if (pickedFile == null) return; + // Enable Android Photo Picker if available + if (_pickerPlatform is ImagePickerAndroid) { + (_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true; + } + + try { + final XFile? pickedFile = await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + + if (pickedFile == null) return; - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + image(base64Encode, _image); + } catch (e) { + print('Error picking image: $e'); + } } void cameraImageAndroid(Function(String, File) image) async { - XFile? pickedFile = await picker.pickImage(source: ImageSource.camera, imageQuality: 20); - if (pickedFile == null) return; + if (_pickerPlatform is ImagePickerAndroid) { + (_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true; + } - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + try { + final XFile? pickedFile = await _pickerPlatform.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: 20), + ); + + if (pickedFile == null) return; + + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + image(base64Encode, _image); + } catch (e) { + print('Error taking picture: $e'); + } } class _BottomSheet extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 14ad3913..a27fb251 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,8 @@ dependencies: flutter_callkit_incoming: ^3.0.0 in_app_update: ^4.2.5 # device_preview: ^1.3.1 + image_picker_android: ^0.8.13+16 + image_picker_platform_interface: ^2.10.0 location: ^8.0.1 gms_check: ^1.0.4 From 597b637ca6835f352f44c52469b07c9c46772fed Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Sun, 19 Apr 2026 14:49:03 +0300 Subject: [PATCH 3/5] Fixes & Translation --- lib/widgets/dropdown/country_dropdown_widget.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart index ce1a660f..c1ef39f1 100644 --- a/lib/widgets/dropdown/country_dropdown_widget.dart +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -9,6 +9,7 @@ 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/generated/locale_keys.g.dart'; +import 'dart:ui' as ui; class CustomCountryDropdown extends StatefulWidget { final List countryList; @@ -160,9 +161,13 @@ class CustomCountryDropdownState extends State { AppState appState = getIt.get(); RenderBox renderBox = context.findRenderObject() as RenderBox; Offset offset = renderBox.localToGlobal(Offset.zero); - bool isRtl = appState.getLanguageCode() == "ar"; - double leftPosition = isRtl ? offset.dx + 8 + renderBox.size.width - (!widget.isFromBottomSheet ? renderBox.size.width : 60.h) : offset.dx; + double leftPosition; + if (isRtl) { + leftPosition = offset.dx; + } else { + leftPosition = offset.dx; + } _overlayEntry = OverlayEntry( builder: (context) => Stack( From 344c9de29f70c455820be40959f804d3d18bb521 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 19 Apr 2026 14:49:21 +0300 Subject: [PATCH 4/5] updates --- .../tele_consultation/zoom/call_screen.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index 3e7720a1..f499bba0 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -92,6 +92,18 @@ class _CallScreenState extends State { useEffect(() { Future.microtask(() async { + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; + if (cameraGranted && micGranted) { + } else { + await LiveCarePermissionService.instance.showOpenSettingsDialog( + context, + title: LocaleKeys.permissionsProfile.tr(), + message: LocaleKeys.liveCarePermissions.tr(), + ); + } + var token = generateJwt(args.sessionName, args.role); Utils.removeFromPrefs(CacheConst.isAppOpenedFromCall); From 2fd4a3e1a774538e3ca82d8966b6317a5a08bee3 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Sun, 19 Apr 2026 17:06:11 +0300 Subject: [PATCH 5/5] Updates --- .../immediate_livecare_payment_details.dart | 24 ++++++++++--------- .../tele_consultation/zoom/call_screen.dart | 6 ++--- lib/services/livecare_permission_service.dart | 15 ++++++++---- 3 files changed, 27 insertions(+), 18 deletions(-) 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 4bb1a0fd..81d232fa 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -239,8 +239,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency ?? "ريال").toLowerCase() == "ريال")), ], ).paddingSymmetrical(24.h, 0.h), - // (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") - (true) + (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total == "0.0") + // (true) ? CustomButton( text: LocaleKeys.confirmLiveCare.tr(context: context), onPressed: () async { @@ -357,24 +357,25 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } Future askVideoCallPermission(BuildContext context) async { - final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(false, context); // If service returned nothing (error), treat as not granted if (statuses.isEmpty) return false; - bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; - bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; + // bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; + // bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; bool notifGranted = statuses[Permission.notification]?.isGranted ?? false; // bool alertWindowGranted = Platform.isAndroid ? (statuses[Permission.systemAlertWindow]?.isGranted ?? false) : true; bool alertWindowGranted = true; // If all required permissions are already granted - if (cameraGranted && micGranted && notifGranted && alertWindowGranted) return true; + // if (cameraGranted && micGranted && notifGranted && alertWindowGranted) return true; + if (notifGranted && alertWindowGranted) return true; // Collect only the missing permissions final missing = []; - if (!cameraGranted) missing.add(Permission.camera); - if (!micGranted) missing.add(Permission.microphone); + // if (!cameraGranted) missing.add(Permission.camera); + // if (!micGranted) missing.add(Permission.microphone); if (!notifGranted) missing.add(Permission.notification); if (Platform.isAndroid && !alertWindowGranted) missing.add(Permission.systemAlertWindow); @@ -415,8 +416,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } // Update grant state (preserve previously granted ones) - cameraGranted = newStatuses[Permission.camera]?.isGranted ?? cameraGranted; - micGranted = newStatuses[Permission.microphone]?.isGranted ?? micGranted; + // cameraGranted = newStatuses[Permission.camera]?.isGranted ?? cameraGranted; + // micGranted = newStatuses[Permission.microphone]?.isGranted ?? micGranted; notifGranted = newStatuses[Permission.notification]?.isGranted ?? notifGranted; alertWindowGranted = newStatuses[Permission.systemAlertWindow]?.isGranted ?? alertWindowGranted; @@ -435,7 +436,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { } // return cameraGranted && micGranted && notifGranted && alertWindowGranted; - return cameraGranted && micGranted && notifGranted; + // return cameraGranted && micGranted && notifGranted; + return notifGranted; } // Future askVideoCallPermission() async { diff --git a/lib/presentation/tele_consultation/zoom/call_screen.dart b/lib/presentation/tele_consultation/zoom/call_screen.dart index f499bba0..2d3e9dbf 100644 --- a/lib/presentation/tele_consultation/zoom/call_screen.dart +++ b/lib/presentation/tele_consultation/zoom/call_screen.dart @@ -92,7 +92,7 @@ class _CallScreenState extends State { useEffect(() { Future.microtask(() async { - final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(true, context); bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; if (cameraGranted && micGranted) { @@ -623,7 +623,7 @@ class _CallScreenState extends State { } void onPressAudio() async { - final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(true, context); bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; @@ -650,7 +650,7 @@ class _CallScreenState extends State { } void onPressVideo() async { - final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(context); + final statuses = await LiveCarePermissionService.instance.requestCameraMicAndNotification(true, context); bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; diff --git a/lib/services/livecare_permission_service.dart b/lib/services/livecare_permission_service.dart index b8d71ff1..bc5f2eef 100644 --- a/lib/services/livecare_permission_service.dart +++ b/lib/services/livecare_permission_service.dart @@ -18,7 +18,7 @@ class LiveCarePermissionService { /// Requests camera, microphone and notification permissions together and /// returns the final map of permission statuses. /// This method does NOT show any dialogs — UI decisions must be done by caller. - Future> requestCameraMicAndNotification(BuildContext context) async { + Future> requestCameraMicAndNotification(bool isCheckCameraMicPermissions, BuildContext context) async { if (_ongoingRequest != null) { return _ongoingRequest!.future; } @@ -26,10 +26,17 @@ class LiveCarePermissionService { _ongoingRequest = Completer>(); final completer = _ongoingRequest!; - final permissions = [ - Permission.camera, + final permissions = isCheckCameraMicPermissions + ? [ + Permission.camera, Permission.microphone, - Permission.notification, + Permission.notification, + if (Platform.isAndroid) Permission.systemAlertWindow, + ] + : [ + // Permission.camera, + // Permission.microphone, + Permission.notification, if (Platform.isAndroid) Permission.systemAlertWindow, ];