From 5f559a44dda76d7d30837d723789b7ab70f9c043 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 9 Apr 2026 13:44:54 +0300 Subject: [PATCH 1/4] profile image upload --- android/app/src/main/AndroidManifest.xml | 13 +- assets/images/svg/gallery.svg | 14 +- assets/images/svg/open_camera.svg | 9 +- assets/langs/ar-SA.json | 16 +- assets/langs/en-US.json | 16 +- ios/Podfile.lock | 161 ++++-- ios/Runner/Info.plist | 2 + lib/core/api_consts.dart | 6 +- lib/core/app_assets.dart | 3 + lib/core/app_state.dart | 6 + lib/core/utils/image_compression_helper.dart | 103 ++++ .../authentication_view_model.dart | 44 ++ .../profile_settings_repo.dart | 96 ++++ .../profile_settings_view_model.dart | 119 +++++ lib/generated/locale_keys.g.dart | 14 + lib/presentation/home/landing_page.dart | 16 +- .../home/widgets/welcome_widget.dart | 11 +- .../medical_file/medical_file_page.dart | 12 +- .../widgets/medical_file_card.dart | 11 +- .../my_family/widget/family_cards.dart | 22 +- .../profile_settings/profile_settings.dart | 28 +- .../widgets/family_card_widget.dart | 9 +- .../widgets/profile_picture_widget.dart | 499 ++++++++++++++++++ lib/widgets/attachment_options.dart | 110 ++-- lib/widgets/image_picker.dart | 72 ++- lib/widgets/user_avatar_widget.dart | 93 ++++ pubspec.yaml | 1 + 27 files changed, 1310 insertions(+), 196 deletions(-) create mode 100644 lib/core/utils/image_compression_helper.dart create mode 100644 lib/presentation/profile_settings/widgets/profile_picture_widget.dart create mode 100644 lib/widgets/user_avatar_widget.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f703aeb8..0e1749d6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,10 +41,13 @@ tools:node="remove" /> - - - - + + + + + + + - - diff --git a/assets/images/svg/gallery.svg b/assets/images/svg/gallery.svg index ec1c45ef..1387f976 100644 --- a/assets/images/svg/gallery.svg +++ b/assets/images/svg/gallery.svg @@ -1,11 +1,5 @@ - - - - - - - - - - + + + + diff --git a/assets/images/svg/open_camera.svg b/assets/images/svg/open_camera.svg index 171452a0..3dfda03c 100644 --- a/assets/images/svg/open_camera.svg +++ b/assets/images/svg/open_camera.svg @@ -1,7 +1,4 @@ - - - - - - + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 822b73dd..a1d22707 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1691,5 +1691,19 @@ "generateAiAnalysisRadResult": "بإنشاء تحليل الذكاء الاصطناعي لهذا التقرير", "grantLocationPermission": "يرجى منح إذن الوصول إلى الموقع من إعدادات التطبيق للحصول على نتائج أفضل.", "NoBookedAppointments": "لا توجد مواعيد محجوزة", - "aiDisclaimerRad": "سيتم مشاركة بيانات نتائج الأشعة الخاصة بك بشكل آمن مع محلل الذكاء الاصطناعي لدينا لتحليلها. يساعد هذا في توفير رؤى صحية مخصصة. هل ترغب في المتابعة؟" + "aiDisclaimerRad": "سيتم مشاركة بيانات نتائج الأشعة الخاصة بك بشكل آمن مع محلل الذكاء الاصطناعي لدينا لتحليلها. يساعد هذا في توفير رؤى صحية مخصصة. هل ترغب في المتابعة؟", + "imageSizeTooLarge": "حجم الصورة كبير جداً. الحد الأقصى للحجم هو 1 ميجابايت", + "profileImageUpdatedSuccessfully": "تم تحديث صورة الملف الشخصي بنجاح", + "failedToProcessImage": "فشل في معالجة الصورة. يرجى المحاولة مرة أخرى", + "cameraGalleryPermissionMessage": "مطلوب إذن الكاميرا والمعرض لتحميل صورة الملف الشخصي. يرجى منح الأذونات من إعدادات التطبيق.", + "cameraGalleryPermissionRequired": "مطلوب أذونات الكاميرا والمعرض", + "cameraPermissionMessage": "مطلوب إذن الكاميرا لالتقاط الصور. يرجى منح الإذن من إعدادات التطبيق.", + "cameraPermissionRequired": "مطلوب إذن الكاميرا", + "galleryPermissionMessage": "مطلوب إذن المعرض لتحديد الصور. يرجى منح الإذن من إعدادات التطبيق.", + "galleryPermissionRequired": "مطلوب إذن المعرض", + "failedToCheckPermissions": "فشل في التحقق من الأذونات. يرجى المحاولة مرة أخرى", + "takePhotoWithCamera": "التقط صورة بالكاميرا", + "selectFromPhotoLibrary": "اختر من مكتبة الصور", + "selectDocumentFiles": "اختر ملفات المستندات", + "profilePicture": "صورة الملف الشخصي" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 27a67857..3b1070d9 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1683,5 +1683,19 @@ "generateAiAnalysisRadResult": "Generate AI analysis for this result", "grantLocationPermission": "Please grant location permission from app settings to see better results.", "NoBookedAppointments": "No Booked Appointments", - "aiDisclaimerRad": "Your radiology result data will be securely shared with our AI Analyzer for analysis. This helps provide personalized health insights. Do you want to proceed?" + "aiDisclaimerRad": "Your radiology result data will be securely shared with our AI Analyzer for analysis. This helps provide personalized health insights. Do you want to proceed?", + "imageSizeTooLarge": "Image size is too large. Maximum size is 1MB", + "profileImageUpdatedSuccessfully": "Profile image updated successfully", + "failedToProcessImage": "Failed to process image. Please try again", + "cameraGalleryPermissionMessage": "Camera and Gallery permissions are required to upload profile picture. Please grant permissions from app settings.", + "cameraGalleryPermissionRequired": "Camera and Gallery permissions are required", + "cameraPermissionMessage": "Camera permission is required to take photos. Please grant permission from app settings.", + "cameraPermissionRequired": "Camera permission is required", + "galleryPermissionMessage": "Gallery permission is required to select photos. Please grant permission from app settings.", + "galleryPermissionRequired": "Gallery permission is required", + "failedToCheckPermissions": "Failed to check permissions. Please try again", + "takePhotoWithCamera": "Take a photo with your camera", + "selectFromPhotoLibrary": "Select from your photo library", + "selectDocumentFiles": "Select document files", + "profilePicture": "Profile Picture" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 02c8b5da..deb3a53f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,9 @@ PODS: - FirebaseAnalytics (~> 11.15.0) - Firebase/CoreOnly (11.15.0): - FirebaseCore (~> 11.15.0) + - Firebase/Crashlytics (11.15.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 11.15.0) - Firebase/Messaging (11.15.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.15.0) @@ -67,6 +70,10 @@ PODS: - firebase_core (3.15.2): - Firebase/CoreOnly (= 11.15.0) - Flutter + - firebase_crashlytics (4.3.10): + - Firebase/Crashlytics (= 11.15.0) + - firebase_core + - Flutter - firebase_messaging (15.2.10): - Firebase/Messaging (= 11.15.0) - firebase_core @@ -93,8 +100,19 @@ PODS: - FirebaseCoreInternal (~> 11.15.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (11.15.0): + - FirebaseCore (~> 11.15.0) - FirebaseCoreInternal (11.15.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseCrashlytics (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSessions (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) - FirebaseInstallations (11.15.0): - FirebaseCore (~> 11.15.0) - GoogleUtilities/Environment (~> 8.1) @@ -109,11 +127,26 @@ PODS: - GoogleUtilities/Reachability (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - nanopb (~> 3.30910.0) + - FirebaseRemoteConfigInterop (11.15.0) + - FirebaseSessions (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseCoreExtension (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) - FLAnimatedImage (1.0.17) - Flutter (1.0.0) - flutter_callkit_incoming (0.0.1): - CryptoSwift - Flutter + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder - flutter_inappwebview_ios (0.0.1): - Flutter - flutter_inappwebview_ios/Core (= 0.0.1) @@ -209,9 +242,23 @@ PODS: - Flutter - image_picker_ios (0.0.1): - Flutter + - in_app_review (2.0.0): + - Flutter - just_audio (0.0.1): - Flutter - FlutterMacOS + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -219,6 +266,9 @@ PODS: - Flutter - manage_calendar_events (0.0.1): - Flutter + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) - map_launcher (0.0.1): - Flutter - MapboxCommon (23.11.0) @@ -264,9 +314,14 @@ PODS: - Flutter - Polyline (5.1.0) - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) - SDWebImage (5.21.5): - SDWebImage/Core (= 5.21.5) - SDWebImage/Core (5.21.5) + - SDWebImageWebPCoder (0.15.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -305,10 +360,12 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - FLAnimatedImage - Flutter (from `Flutter`) - flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_ios_voip_kit_karmm (from `.symlinks/plugins/flutter_ios_voip_kit_karmm/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) @@ -319,6 +376,7 @@ DEPENDENCIES: - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - health (from `.symlinks/plugins/health/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - just_audio (from `.symlinks/plugins/just_audio/darwin`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - location (from `.symlinks/plugins/location/ios`) @@ -347,9 +405,13 @@ SPEC REPOS: - Firebase - FirebaseAnalytics - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseCrashlytics - FirebaseInstallations - FirebaseMessaging + - FirebaseRemoteConfigInterop + - FirebaseSessions - FLAnimatedImage - Google-Maps-iOS-Utils - GoogleAdsOnDeviceConversion @@ -357,6 +419,8 @@ SPEC REPOS: - GoogleDataTransport - GoogleMaps - GoogleUtilities + - libwebp + - Mantle - MapboxCommon - MapboxCoreMaps - MapboxCoreNavigation @@ -371,7 +435,9 @@ SPEC REPOS: - PayFortSDK - Polyline - PromisesObjC + - PromisesSwift - SDWebImage + - SDWebImageWebPCoder - Solar-dev - SwiftProtobuf - SwiftyGif @@ -399,12 +465,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" firebase_messaging: :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter flutter_callkit_incoming: :path: ".symlinks/plugins/flutter_callkit_incoming/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_ios_voip_kit_karmm: @@ -425,6 +495,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/health/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + in_app_review: + :path: ".symlinks/plugins/in_app_review/ios" just_audio: :path: ".symlinks/plugins/just_audio/darwin" local_auth_darwin: @@ -461,50 +533,59 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - amazon_payfort: 4ad7a3413acc1c4c4022117a80d18fee23c572d3 - audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 - barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966 - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + amazon_payfort: 782d287860ac8962baf4e0a2a0fb2f81fdf27204 + audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe + barcode_scan2: 050b7b327e85954c8918715d42e83c5a35c3ed89 + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed - device_calendar_plus_ios: 2c04ad7643c6e697438216e33693b84e8ca45ded - device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b + device_calendar_plus_ios: 34dca387f44fad29b840096a840f94edc55af858 + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e - firebase_analytics: 0e25ca1d4001ccedd40b4e5b74c0ec34e18f6425 - firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 - firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707 + firebase_analytics: bf93e20703c95030404d6ddbb1adf05bf5c3885b + firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c + firebase_crashlytics: 986773c0c7ea4c3f5a4ead141b58522fcd9dfdb1 + firebase_messaging: bf6697c61f31c7cc0f654131212ff04c0115c2c7 FirebaseAnalytics: 6433dfd311ba78084fc93bdfc145e8cb75740eae FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e + FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435 FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 + FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 + FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa + FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7 FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_ios_voip_kit_karmm: 371663476722afb631d5a13a39dee74c56c1abd0 - flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - flutter_nfc_kit: e1b71583eafd2c9650bc86844a7f2d185fb414f6 - flutter_zoom_videosdk: 0f59e71685a03ddb0783ecc43bf3155b8599a7f5 - fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 - geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + flutter_callkit_incoming: 417dd1b46541cdd5d855ad795ccbe97d1c18155e + flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_ios_voip_kit_karmm: 7ea37381a8841c92d186edf1f4604df5cc437579 + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_nfc_kit: 3985c93f749b9cb4747479205c2f10bd2f877a11 + flutter_zoom_videosdk: 7ddc97d617b85aeace252165d2b5fc5638678c38 + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f + geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 - google_maps_flutter_ios: 3213e1e5f5588b6134935cb8fc59acb4e6d88377 + google_maps_flutter_ios: ffdd933832966673a1f57f5effd014dc8d4d43a2 GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 GoogleAppMeasurement: 700dce7541804bec33db590a5c496b663fbe2539 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - health: 32d2fbc7f26f9a2388d1a514ce168adbfa5bda65 - image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 - just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed - local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb - location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8 - manage_calendar_events: fe1541069431af035ced925ebd9def8b4b271254 - map_launcher: 8051ad5783913cafce93f2414c6858f2904fd8df + health: 9cd70a5c9d9ec559a12d639a1471091c0cbb6b48 + image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b + in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc + just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100 + location: d5cf8598915965547c3f36761ae9cc4f4e87d22e + manage_calendar_events: 9b2889799340398027b3e3f5c4891d41599ec257 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + map_launcher: 8a4e484aaddff55680a7da9bc9e25f512067fb87 MapboxCommon: 119f3759f7dc9457f0695848108ab323eb643cb4 MapboxCoreMaps: ca17f67baced23f8c952166ac6314c35bad3f66c MapboxCoreNavigation: 3be9990fae3ed732a101001746d0e3b4234ec023 @@ -515,27 +596,29 @@ SPEC CHECKSUMS: MapboxNavigationNative: 629e359f3d2590acd1ebbacaaf99e1a80ee57e42 MapboxSpeech: cd25ef99c3a3d2e0da72620ff558276ea5991a77 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba PayFortSDK: 233eabe9a45601fdbeac67fa6e5aae46ed8faf82 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e - url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b - video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a - wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 - webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa + video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86 + wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 ZoomVideoSDK: 94e939820e57a075c5e712559f927017da0de06a PODFILE CHECKSUM: 8235407385ddd5904afc2563d65406117a51993e diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index bbe54c9a..5b3b2681 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -89,6 +89,8 @@ This app requires motion detection access to function properly. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. + NSPhotoLibraryAddUsageDescription + This app requires photo library access to upload profile picture and documents. NSRemindersUsageDescription This app requires calendar access to set reminders for Virtual & Normal Appointments. NSSpeechRecognitionUsageDescription diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9a16cdbc..c74d9fd5 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.preProd; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -945,6 +945,10 @@ const PROFILE_SETTING = 'Services/Patients.svc/REST/GetPateintInfoForUpdate'; const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo'; +const GET_PROFILE_IMAGE = 'Services/Patients.svc/REST/Patient_GetProfileImageData'; + +const UPLOAD_PROFILE_IMAGE = 'Services/Patients.svc/REST/Patient_InsertProfileImageData'; + const DOWNLOAD_INVOICE_PDF = 'Services/Notifications.svc/REST/DownloadInvoiceReport'; class ApiKeyConstants { diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 21280f45..14227fa6 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -357,6 +357,9 @@ class AppAssets { static const String biometricLockIcon = '$svgBasePath/biometric_lock_icon.svg'; static const String imageIcon = '$svgBasePath/image_icon.svg'; static const String appointmentRatingIcon = '$svgBasePath/appointment_rating_icon.svg'; + static const String open_camera = '$svgBasePath/open_camera.svg'; + static const String gallery = '$svgBasePath/gallery.svg'; + static const String files = '$svgBasePath/files.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index a5a8d72a..5bbf19aa 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -84,6 +84,12 @@ class AppState { set setUserBloodGroup(String value) => _userBloodGroup = value; + String? _profileImageData; + + String? get getProfileImageData => _profileImageData; + + set setProfileImageData(String? value) => _profileImageData = value; + SelectDeviceByImeiRespModelElement? _selectDeviceByImeiRespModelElement; void setSelectDeviceByImeiRespModelElement(SelectDeviceByImeiRespModelElement? value) { diff --git a/lib/core/utils/image_compression_helper.dart b/lib/core/utils/image_compression_helper.dart new file mode 100644 index 00000000..71567621 --- /dev/null +++ b/lib/core/utils/image_compression_helper.dart @@ -0,0 +1,103 @@ +import 'dart:io'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; + +class ImageCompressionHelper { + /// Compress and resize image to max 200x200 and ensure file size < 1MB + /// Returns the compressed image file + static Future compressAndResizeImage(File imageFile) async { + try { + // Check if file exists + if (!await imageFile.exists()) { + print('Image file does not exist'); + return null; + } + + print('Original file size: ${await imageFile.length() / 1024} KB'); + + // Get temporary directory + final tempDir = await getTemporaryDirectory(); + final targetPath = path.join( + tempDir.path, + 'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg', + ); + + // Start with quality 85 + int quality = 85; + const int maxFileSizeBytes = 1048576; // 1MB + + // Compress the image + var result = await FlutterImageCompress.compressWithFile( + imageFile.absolute.path, + quality: quality, + format: CompressFormat.jpeg, + ); + + if (result == null || result.isEmpty) { + print('Compression returned null or empty'); + return null; + } + + // Write to file + File resultFile = File(targetPath); + await resultFile.writeAsBytes(result); + + int fileSize = await resultFile.length(); + print('First compression: ${fileSize / 1024} KB, quality: $quality'); + + // If file size is still > 1MB, reduce quality progressively + int attempts = 0; + while (fileSize > maxFileSizeBytes && quality > 10 && attempts < 10) { + quality -= 10; + attempts++; + + result = await FlutterImageCompress.compressWithFile( + imageFile.absolute.path, + quality: quality, + format: CompressFormat.jpeg, + ); + + if (result == null || result.isEmpty) { + print('Compression returned null on attempt $attempts'); + break; + } + + await resultFile.writeAsBytes(result); + fileSize = await resultFile.length(); + print('Compression attempt $attempts: ${fileSize / 1024} KB, quality: $quality'); + } + + // Final validation + if (await resultFile.exists()) { + final finalSize = await resultFile.length(); + print('Final compressed image size: ${finalSize / 1024} KB'); + + if (finalSize > maxFileSizeBytes) { + print('Warning: Final file size (${finalSize / 1024} KB) exceeds 1MB limit'); + } + + return resultFile; + } + + return null; + } catch (e, stackTrace) { + print('Error compressing image: $e'); + print('Stack trace: $stackTrace'); + return null; + } + } + + /// Get file size in KB + static Future getFileSizeInKB(File file) async { + int bytes = await file.length(); + return bytes / 1024; + } + + /// Get file size in MB + static Future getFileSizeInMB(File file) async { + int bytes = await file.length(); + return bytes / (1024 * 1024); + } +} + diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 7e753722..2e9426a0 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -47,6 +47,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; import '../insurance/insurance_view_model.dart'; +import '../profile_settings/profile_settings_repo.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; import 'models/request_models/insert_patient_mobile_deviceinfo.dart'; import 'models/request_models/patient_insert_device_imei_request.dart'; @@ -646,6 +647,9 @@ class AuthenticationViewModel extends ChangeNotifier { activation.list!.first.isParentUser = true; _appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true); } + + // Fetch profile image after switching user + _fetchProfileImage(activation.list!.first.patientId); } else { activation.list!.first.isParentUser = true; _appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true); @@ -656,6 +660,9 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; + // Fetch profile image after login + _fetchProfileImage(activation.list!.first.patientId); + // Fetch patient blood type from new API // await getPatientBloodTypeNew(); } @@ -1215,4 +1222,41 @@ class AuthenticationViewModel extends ChangeNotifier { log("Error calling getPatientBloodType: $e"); } } + + /// Fetch profile image after login and store in AppState + Future _fetchProfileImage(int? patientID) async { + if (patientID == null) return; + + // Skip if already loaded in AppState + if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { + log("Profile image already cached, skipping API call"); + return; + } + + try { + final profileSettingsRepo = GetIt.instance(); + final result = await profileSettingsRepo.getProfileImage(patientID: patientID); + + result.fold( + (failure) { + // Silently fail - profile image is optional + log("Failed to fetch profile image: ${failure.message}"); + }, + (apiResponse) { + if (apiResponse.data != null && apiResponse.data['Patient_GetProfileImageDataList'] != null) { + var imageList = apiResponse.data['Patient_GetProfileImageDataList']; + if (imageList is List && imageList.isNotEmpty) { + String? imageData = imageList[0]['PatientsProfileImageData']; + if (imageData != null && imageData.isNotEmpty) { + _appState.setProfileImageData = imageData; + log("Profile image loaded successfully"); + } + } + } + }, + ); + } catch (e) { + log("Error fetching profile image: $e"); + } + } } diff --git a/lib/features/profile_settings/profile_settings_repo.dart b/lib/features/profile_settings/profile_settings_repo.dart index 9d4c1221..d86bd7fd 100644 --- a/lib/features/profile_settings/profile_settings_repo.dart +++ b/lib/features/profile_settings/profile_settings_repo.dart @@ -16,6 +16,18 @@ abstract class ProfileSettingsRepo { Future>> deactivateAccount(); Future>> getProfileSettings(); + + /// Get patient profile image + Future>> getProfileImage({ + required int patientID, + }); + + /// Upload patient profile image + Future>> uploadProfileImage({ + required int patientID, + required String imageData, + required bool isActive, + }); } class ProfileSettingsRepoImp implements ProfileSettingsRepo { @@ -137,5 +149,89 @@ class ProfileSettingsRepoImp implements ProfileSettingsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getProfileImage({ + required int patientID, + }) async { + final Map body = { + 'PatientID': patientID, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + GET_PROFILE_IMAGE, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> uploadProfileImage({ + required int patientID, + required String imageData, + required bool isActive, + }) async { + final Map body = { + 'PatientID': patientID, + 'PatientsProfileImageData': imageData, + 'IsActive': isActive, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + UPLOAD_PROFILE_IMAGE, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 28f28af6..77960569 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -41,6 +42,12 @@ class ProfileSettingsViewModel extends ChangeNotifier { late GetPatientInfoForUpdate getPatientInfoForUpdate; bool isPatientProfileLoading = false; + // ── Profile image state ────────────────────────────────────────────── + bool isProfileImageLoading = false; + bool isUploadingProfileImage = false; + String? profileImageData; + String? profileImageError; + ProfileSettingsViewModel({ required CacheService cacheService, required this.profileSettingsRepo, @@ -172,4 +179,116 @@ class ProfileSettingsViewModel extends ChangeNotifier { isCloseButtonVisible: true, ); } + + // ── Profile image ──────────────────────────────────────────────────── + + Future getProfileImage({ + required int patientID, + Function(dynamic)? onSuccess, + Function(String)? onError, + bool forceRefresh = false, // Add flag to force refresh + }) async { + // Skip API call if image is already loaded and not forcing refresh + if (!forceRefresh && GetIt.instance().getProfileImageData != null) { + // Use cached data + profileImageData = GetIt.instance().getProfileImageData; + if (onSuccess != null) { + onSuccess(profileImageData); + } + return; + } + + isProfileImageLoading = true; + profileImageError = null; + notifyListeners(); + + final result = await profileSettingsRepo.getProfileImage(patientID: patientID); + + result.fold( + (failure) { + isProfileImageLoading = false; + profileImageError = failure.message; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } + }, + (response) { + isProfileImageLoading = false; + // Extract image data from response + if (response.data != null && response.data['Patient_GetProfileImageDataList'] != null) { + var imageList = response.data['Patient_GetProfileImageDataList']; + if (imageList is List && imageList.isNotEmpty) { + profileImageData = imageList[0]['PatientsProfileImageData']; + // Store in AppState for global access + GetIt.instance().setProfileImageData = profileImageData; + } else { + profileImageData = null; + GetIt.instance().setProfileImageData = null; + } + } else { + profileImageData = null; + GetIt.instance().setProfileImageData = null; + } + notifyListeners(); + onSuccess?.call(response.data); + }, + ); + } + + /// Clear cached profile image data + void clearProfileImageCache() { + profileImageData = null; + GetIt.instance().setProfileImageData = null; + notifyListeners(); + } + + /// Sync profile image data from AppState + void syncProfileImageFromAppState() { + final appStateImageData = GetIt.instance().getProfileImageData; + if (appStateImageData != null && appStateImageData.isNotEmpty) { + if (profileImageData != appStateImageData) { + profileImageData = appStateImageData; + notifyListeners(); + } + } + } + + Future uploadProfileImage({ + required int patientID, + required String imageData, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isUploadingProfileImage = true; + profileImageError = null; + notifyListeners(); + + final result = await profileSettingsRepo.uploadProfileImage( + patientID: patientID, + imageData: imageData, + isActive: true, + ); + + result.fold( + (failure) { + isUploadingProfileImage = false; + profileImageError = failure.message; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (response) { + isUploadingProfileImage = false; + profileImageData = imageData; + // Store in AppState for global access + GetIt.instance().setProfileImageData = imageData; + notifyListeners(); + onSuccess?.call(response.data); + }, + ); + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 29656b23..cd917683 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1683,5 +1683,19 @@ abstract class LocaleKeys { static const grantLocationPermission = 'grantLocationPermission'; static const NoBookedAppointments = 'NoBookedAppointments'; static const aiDisclaimerRad = 'aiDisclaimerRad'; + static const imageSizeTooLarge = 'imageSizeTooLarge'; + static const profileImageUpdatedSuccessfully = 'profileImageUpdatedSuccessfully'; + static const failedToProcessImage = 'failedToProcessImage'; + static const cameraGalleryPermissionMessage = 'cameraGalleryPermissionMessage'; + static const cameraGalleryPermissionRequired = 'cameraGalleryPermissionRequired'; + static const cameraPermissionMessage = 'cameraPermissionMessage'; + static const cameraPermissionRequired = 'cameraPermissionRequired'; + static const galleryPermissionMessage = 'galleryPermissionMessage'; + static const galleryPermissionRequired = 'galleryPermissionRequired'; + static const failedToCheckPermissions = 'failedToCheckPermissions'; + static const takePhotoWithCamera = 'takePhotoWithCamera'; + static const selectFromPhotoLibrary = 'selectFromPhotoLibrary'; + static const selectDocumentFiles = 'selectDocumentFiles'; + static const profilePicture = 'profilePicture'; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 371fe892..298bca81 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -48,6 +48,7 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; +import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; @@ -237,16 +238,11 @@ class _LandingPageState extends State { ); }, name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), - // imageUrl: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, - imageUrl: appState - .getAuthenticatedUser() - ?.gender == 1 - ? ((appState - .getAuthenticatedUser() - ?.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg) - : ((appState - .getAuthenticatedUser() - ?.age ?? 0) < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), + imageWidget: UserAvatarWidget( + width: 42.w, + height: 42.h, + fit: BoxFit.cover, + ), ).expanded : CustomButton( text: LocaleKeys.loginOrRegister.tr(context: context), diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 170eb4e3..3131fbbf 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -20,13 +20,15 @@ import 'package:smooth_corner/smooth_corner.dart'; class WelcomeWidget extends StatelessWidget { final String name; - final String imageUrl; + final String? imageUrl; + final Widget? imageWidget; final VoidCallback? onTap; const WelcomeWidget({ super.key, required this.name, - required this.imageUrl, + this.imageUrl, + this.imageWidget, this.onTap, }); @@ -50,7 +52,10 @@ class WelcomeWidget extends StatelessWidget { ), ); }), - Image.asset(imageUrl, width: 40, height: 40), + imageWidget ?? + (imageUrl != null + ? Image.asset(imageUrl!, width: 40, height: 40) + : SizedBox(width: 40, height: 40)), Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 4.h, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index d04f2ac8..76c0d083 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -78,6 +78,7 @@ import 'package:hmg_patient_app_new/widgets/input_widget.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:hmg_patient_app_new/widgets/shimmer/common_shimmer_widget.dart'; +import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -242,12 +243,11 @@ class _MedicalFilePageState extends State { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset( - (appState.getAuthenticatedUser()?.gender == 1 - ? ((appState.getAuthenticatedUser()?.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg) - : ((appState.getAuthenticatedUser()?.age ?? 0) < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg)), - width: 56.w, - height: 56.h), + UserAvatarWidget( + width: 56.w, + height: 56.h, + fit: BoxFit.cover, + ), SizedBox(width: 8.w), 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 84f977ce..40ae17d4 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -12,6 +12,7 @@ class MedicalFileCard extends StatelessWidget { final double? iconSize; final bool isLargeText; final bool isLocked; + final Color? iconColor; const MedicalFileCard({ super.key, @@ -22,6 +23,7 @@ class MedicalFileCard extends StatelessWidget { this.iconSize, this.isLargeText = false, this.isLocked = false, + this.iconColor, }); @override @@ -44,7 +46,14 @@ class MedicalFileCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: svgIcon, width: iconS, height: iconS, fit: BoxFit.contain, applyThemeColor: false), + Utils.buildSvgWithAssets( + icon: svgIcon, + width: iconS, + height: iconS, + fit: BoxFit.contain, + applyThemeColor: false, + iconColor: iconColor, + ), SizedBox(height: 8.h), isLargeText ? label.toText13(color: textColor, isBold: true, maxLine: 2) : label.toText11(color: textColor, isBold: true, maxLine: 2), ], diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 39344f30..1938daba 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -23,6 +23,7 @@ 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/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; import 'package:provider/provider.dart'; class FamilyCards extends StatefulWidget { @@ -176,12 +177,11 @@ class _FamilyCardsState extends State { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset( - (appState.getAuthenticatedUser()?.gender == 1 - ? ((appState.getAuthenticatedUser()?.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg) - : ((appState.getAuthenticatedUser()?.age ?? 0) < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg)), - width: 56.w, - height: 56.h), + UserAvatarWidget( + width: 56.w, + height: 56.h, + fit: BoxFit.cover, + ), SizedBox(width: 8.w), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -334,14 +334,12 @@ class _FamilyCardsState extends State { Column( mainAxisSize: MainAxisSize.min, children: [ - Utils.buildImgWithAssets( - icon: profile.gender == null - ? AppAssets.dummyUser - : profile.gender == 1 - ? ((profile.age ?? 0) < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg) - : (profile.age! < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg), + UserAvatarWidget( width: 72.h, height: 70.h, + gender: profile.gender, + age: profile.age, + fit: BoxFit.cover, ), SizedBox(height: 8.h), (profile.patientName ?? "Unknown").toText14(isBold: true, isCenter: true, maxlines: 1), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 127f9a3e..0abcfd81 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -8,8 +7,6 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.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/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -17,28 +14,26 @@ 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/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/models/feedback_type.dart'; -import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; -import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; -import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; +import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/profile_picture_widget.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_email_widget.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_emergency_contact_widget.dart'; -import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/app_language_change.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/user_avatar_widget.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'; +import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:in_app_review/in_app_review.dart'; @@ -120,6 +115,9 @@ class ProfileSettingsState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Profile Picture Section + ProfilePictureWidget(), + // SizedBox( // height: dynamicItemHeight(context) + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space // child: Swiper( @@ -455,7 +453,13 @@ class FamilyCardWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h), + UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: profile.gender, + age: profile.age, + fit: BoxFit.cover, + ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -560,10 +564,10 @@ class FamilyCardWidget extends StatelessWidget { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColor.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive - ? AppColors.successColor.withOpacity(0.1) - : AppColors.warningColorYellow.withOpacity(0.1), + ? AppColors.successColor.withValues(alpha: 0.1) + : AppColors.warningColorYellow.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), ).toShimmer2(isShow: insuranceVM.isInsuranceLoading) : SizedBox.shrink(); diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index ffa6644b..97039de1 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -14,6 +14,7 @@ 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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; import 'package:provider/provider.dart'; class FamilyCardWidget extends StatelessWidget { @@ -49,7 +50,13 @@ class FamilyCardWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset(profile.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 56.w, height: 56.h), + UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: profile.gender, + age: profile.age, + fit: BoxFit.cover, + ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart new file mode 100644 index 00000000..7cbeb0ea --- /dev/null +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -0,0 +1,499 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/utils.dart'; +import 'package:hmg_patient_app_new/core/utils/image_compression_helper.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_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/image_picker.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; + +class ProfilePictureWidget extends StatefulWidget { + const ProfilePictureWidget({super.key}); + + @override + State createState() => _ProfilePictureWidgetState(); +} + +class _ProfilePictureWidgetState extends State { + final AppState _appState = getIt.get(); + File? _selectedImage; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Sync ViewModel with AppState first + final profileVm = context.read(); + profileVm.syncProfileImageFromAppState(); + + _loadProfileImage(); + }); + } + + void _loadProfileImage() { + // Check if profile image is already loaded in AppState + if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { + // Image already loaded, no need to call API + return; + } + + final profileVm = context.read(); + final patientID = _appState.getAuthenticatedUser()?.patientId; + + if (patientID != null) { + profileVm.getProfileImage( + patientID: patientID, + onSuccess: (data) { + // Image loaded successfully + }, + onError: (error) { + // Error loading image + }, + ); + } + } + + void _pickImage() { + // Show image picker options without checking permissions first + ImageOptions.showImageOptionsNew( + context, + false, // Don't show files option, only camera and gallery + (base64String, file) async { + try { + print('=== Starting image processing ==='); + print('File path: ${file.path}'); + print('File exists: ${await file.exists()}'); + print('Original file size: ${await file.length() / 1024} KB'); + + // Compress and resize the image + print('Calling compressAndResizeImage...'); + final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file); + + File finalFile; + String finalBase64; + + if (compressedFile == null) { + print('⚠️ Compression failed - using original file as fallback'); + + // Fallback: use original image if compression fails + final originalSize = await file.length(); + final maxSize = 1048576; // 1MB + + if (originalSize > maxSize) { + print('❌ Original file is too large: ${originalSize / 1024} KB'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + print('✅ Using original file (${originalSize / 1024} KB)'); + finalFile = file; + var bytes = await file.readAsBytes(); + finalBase64 = base64.encode(bytes); + } else { + // Check compressed file size + final fileSize = await compressedFile.length(); + final maxSize = 1048576; // 1MB + print('✅ Compression successful: ${fileSize / 1024} KB'); + + if (fileSize > maxSize) { + print('❌ Compressed file still too large'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + finalFile = compressedFile; + var bytes = await compressedFile.readAsBytes(); + finalBase64 = base64.encode(bytes); + } + + print('Converting to base64... Length: ${finalBase64.length}'); + + if (mounted) { + setState(() { + _selectedImage = finalFile; + }); + + print('📤 Starting upload...'); + // Upload the image + _uploadImage(finalBase64); + } + + print('=== Image processing complete ==='); + } catch (e, stackTrace) { + print('❌ Error in _pickImage: $e'); + print('Stack trace: $stackTrace'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToProcessImage.tr(context: context), + ); + } + } + }, + checkCameraPermission: _checkCameraPermission, + checkGalleryPermission: _checkGalleryPermission, + ); + } + + Future _checkCameraPermission() async { + try { + print('=== Checking camera permission ==='); + + // First check current status + PermissionStatus currentStatus = await Permission.camera.status; + print('Current camera permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted) { + print('✅ Camera permission already granted'); + return true; + } + + // If denied or permanently denied, show settings dialog + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking camera permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + Future _checkGalleryPermission() async { + 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; + } + + // First check current status + PermissionStatus currentStatus = await galleryPermission.status; + print('Current gallery permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted || currentStatus.isLimited) { + print('✅ Gallery permission already granted'); + return true; + } + + // If denied or permanently denied, request permission first + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking gallery permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + void _uploadImage(String base64String) { + final profileVm = context.read(); + final patientID = _appState.getAuthenticatedUser()?.patientId; + + if (patientID != null) { + profileVm.uploadProfileImage( + patientID: patientID, + imageData: base64String, + onSuccess: (data) { + if (mounted) { + setState(() { + _selectedImage = null; // Clear selected image after successful upload + }); + Utils.showToast( + LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context), + ); + } + }, + onError: (error) { + Utils.showToast(error); + }, + ); + } + } + + Widget _buildProfileImage(ProfileSettingsViewModel profileVm) { + final gender = _appState.getAuthenticatedUser()?.gender ?? 1; + final age = _appState.getAuthenticatedUser()?.age ?? 0; + + // Determine the default image based on gender and age + final String defaultImage; + if (gender == 1) { + // Male + defaultImage = age < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg; + } else { + // Female + defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg; + } + + // Show selected image if available + if (_selectedImage != null) { + return ClipOval( + child: Image.file( + _selectedImage!, + width: 136.w, + height: 136.h, + fit: BoxFit.cover, + ), + ); + } + + // Check both ViewModel and AppState for profile image + final String? imageData = profileVm.profileImageData ?? _appState.getProfileImageData; + + // Show uploaded image if available + if (imageData != null && imageData.isNotEmpty) { + try { + final bytes = base64Decode(imageData); + return ClipOval( + child: Image.memory( + bytes, + width: 136.w, + height: 136.h, + fit: BoxFit.cover, + ), + ); + } catch (e) { + print('Error decoding profile image: $e'); + // If decoding fails, show default image + return Image.asset( + defaultImage, + width: 136.w, + height: 136.h, + ); + } + } + + // Show default image + return Image.asset( + defaultImage, + width: 136.w, + height: 136.h, + ); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, profileVm, _) { + return Center( + child: Stack( + children: [ + // Profile Image + profileVm.isProfileImageLoading + ? Container( + width: 136.w, + height: 136.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.greyTextColor.withValues(alpha: 0.2), + ), + ).toShimmer2(isShow: true) + : _buildProfileImage(profileVm), + + // Edit button + Positioned( + right: 0, + top: 0, + child: GestureDetector( + onTap: () { + if (!profileVm.isUploadingProfileImage) { + _pickImage(); + } + }, + child: Container( + width: 32.w, + height: 32.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.circle, + border: Border.all( + color: AppColors.whiteColor, + width: 2.w, + ), + ), + child: profileVm.isUploadingProfileImage + ? SizedBox( + width: 16.w, + height: 16.h, + child: CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: AlwaysStoppedAnimation( + AppColors.whiteColor, + ), + ).paddingAll(8.w), + ) + : Icon( + Icons.camera_alt, + color: AppColors.whiteColor, + size: 16.w, + ), + ), + ), + ), + ], + ).paddingOnly(bottom: 24.h), + ); + }, + ); + } +} + diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index 30a0da0c..4d52d1e2 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -9,68 +9,71 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; class AttachmentOptions extends StatelessWidget { - VoidCallback onCameraTap; - VoidCallback onGalleryTap; - VoidCallback onFilesTap; - bool showFilesOption; + final Future Function() onCameraTap; + final Future Function() onGalleryTap; + final VoidCallback onFilesTap; + final bool showFilesOption; - AttachmentOptions({Key? key, required this.onCameraTap, required this.onGalleryTap, required this.onFilesTap, this.showFilesOption = true}) : super(key: key); - - late AppState? appState; + const AttachmentOptions({ + Key? key, + required this.onCameraTap, + required this.onGalleryTap, + required this.onFilesTap, + this.showFilesOption = true + }) : super(key: key); @override Widget build(BuildContext context) { - appState = getIt.get(); - return SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // "Upload Attachment".toSectionHeading(), - // "Select from gallery or open camera".toText11(isBold: true), - checkInOptionCard( - AppAssets.ask_doctor_icon, - LocaleKeys.openCamera.tr(context: context), - "", - ).onPress(() { - onCameraTap(); - }), - SizedBox(height: 16.h), - checkInOptionCard( - AppAssets.ask_doctor_icon, - LocaleKeys.openGallery.tr(context: context), - "", - ).onPress(() { - onGalleryTap(); - }), - SizedBox(height: 16.h), + final appState = getIt.get(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + checkInOptionCard( + appState, + AppAssets.open_camera, + LocaleKeys.openCamera.tr(context: context), + LocaleKeys.takePhotoWithCamera.tr(context: context), + ).onPress(() async { + await onCameraTap(); + }), + SizedBox(height: 16.h), + checkInOptionCard( + appState, + AppAssets.gallery, + LocaleKeys.openGallery.tr(context: context), + LocaleKeys.selectFromPhotoLibrary.tr(context: context), + ).onPress(() async { + await onGalleryTap(); + }), + if (showFilesOption) SizedBox(height: 16.h), + if (showFilesOption) checkInOptionCard( - AppAssets.ask_doctor_icon, + appState, + AppAssets.files, LocaleKeys.openFiles.tr(context: context), - "", - ).onPress(() { + LocaleKeys.selectDocumentFiles.tr(context: context), + ).onPress(() async { onFilesTap(); }), - ], - ).paddingOnly(), + ], ); } - Widget checkInOptionCard(String icon, String title, String subTitle) { + Widget checkInOptionCard(AppState appState, String icon, String title, String subTitle) { return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, - borderRadius: 20.r, + borderRadius: 20.h, hasShadow: false, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Utils.buildSvgWithAssets(icon: icon, width: 24.h, height: 24.h, fit: BoxFit.fill, iconColor: AppColors.textColor), - // SizedBox(height: 16.h), + Utils.buildSvgWithAssets(icon: icon, width: 40.h, height: 40.h, fit: BoxFit.fill), + SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -79,12 +82,12 @@ class AttachmentOptions extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ title.toText16(isBold: true, color: AppColors.textColor), - // subTitle.toText12(isBold: true, color: AppColors.greyTextColor), + subTitle.toText12(isBold: true, color: AppColors.greyTextColor), ], ), ), Transform.flip( - flipX: appState!.isArabic(), + flipX: appState.isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, iconColor: AppColors.blackColor, @@ -99,25 +102,4 @@ class AttachmentOptions extends StatelessWidget { ).paddingAll(16.h), ); } - - Widget itemView(String icon, String title, VoidCallback onTap) { - return InkWell( - onTap: onTap, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SvgPicture.asset( - "assets/images/svg/$icon", - ), - title.toText11(isBold: true), - ], - ).paddingOnly(left: 13, right: 13, top: 16, bottom: 12).expanded.objectContainerBorderView( - disablePadding: true, - radius: 10, - color: AppColors.greyF7Color.withOpacity(.48), - borderColor: AppColors.lightGreyEFColor.withOpacity(.48), - ), - ); - } } diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index c6d35617..f7665a56 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -6,7 +6,6 @@ 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/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/attachment_options.dart'; -import 'package:hmg_patient_app_new/widgets/bottom_sheet.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; @@ -15,37 +14,64 @@ import 'package:image_picker/image_picker.dart'; final ImagePicker picker = ImagePicker(); class ImageOptions { - static void showImageOptionsNew(BuildContext context, bool showFilesOption, Function(String, File) image) { + static void showImageOptionsNew( + BuildContext context, + bool showFilesOption, + Function(String, File) image, { + Future Function()? checkCameraPermission, + Future Function()? checkGalleryPermission, + }) { showCommonBottomSheetWithoutHeight( context, - title: LocaleKeys.feedback.tr(), + title: LocaleKeys.profilePicture.tr(context: context), child: AttachmentOptions( showFilesOption: showFilesOption, onCameraTap: () async { + // Check camera permission if callback provided + if (checkCameraPermission != null) { + final hasPermission = await checkCameraPermission(); + if (!hasPermission) { + return; // Permission denied, don't proceed + } + } + + Navigator.pop(context); // Close bottom sheet + if (Platform.isAndroid) { cameraImageAndroid(image); } else { - File _image = File((await ImagePicker.platform.pickImage(source: ImageSource.camera, imageQuality: 20))?.path ?? ""); - // XFile? media = await picker.pickMedia(); - String? fileName = _image.path; - var bytes = File(fileName!).readAsBytesSync(); + XFile? pickedFile = await picker.pickImage(source: ImageSource.camera, 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); - if (base64Encode != null) { - image(base64Encode, _image); - } + image(base64Encode, _image); } }, onGalleryTap: () async { + // Check gallery permission if callback provided + if (checkGalleryPermission != null) { + final hasPermission = await checkGalleryPermission(); + if (!hasPermission) { + return; // Permission denied, don't proceed + } + } + + Navigator.pop(context); // Close bottom sheet + if (Platform.isAndroid) { galleryImageAndroid(image); } else { - File _image = File((await picker.pickMedia())?.path ?? ""); + XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, 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); - if (base64Encode != null) { - image(base64Encode, _image); - } + image(base64Encode, _image); } }, onFilesTap: () async { @@ -188,25 +214,25 @@ class ImageOptions { } void galleryImageAndroid(Function(String, File) image) async { - File _image = File((await picker.pickMedia())?.path ?? ""); + XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, 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); - if (base64Encode != null) { - image(base64Encode, _image); - } + image(base64Encode, _image); } void cameraImageAndroid(Function(String, File) image) async { - File _image = File((await picker.pickMedia())?.path ?? ""); + XFile? pickedFile = await picker.pickImage(source: ImageSource.camera, 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); - if (base64Encode != null) { - image(base64Encode, _image); - } - + image(base64Encode, _image); } class _BottomSheet extends StatelessWidget { diff --git a/lib/widgets/user_avatar_widget.dart b/lib/widgets/user_avatar_widget.dart new file mode 100644 index 00000000..6a17160a --- /dev/null +++ b/lib/widgets/user_avatar_widget.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; +import 'package:flutter/material.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'; + +class UserAvatarWidget extends StatelessWidget { + final double width; + final double height; + final int? gender; + final int? age; + final BoxFit? fit; + final bool isCircular; + final double? borderRadius; + final String? customProfileImageData; // For family members or other users + + const UserAvatarWidget({ + Key? key, + required this.width, + required this.height, + this.gender, + this.age, + this.fit, + this.isCircular = false, + this.borderRadius, + this.customProfileImageData, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final appState = getIt.get(); + final userGender = gender ?? appState.getAuthenticatedUser()?.gender ?? 1; + final userAge = age ?? appState.getAuthenticatedUser()?.age ?? 0; + + // Use custom profile image data if provided, otherwise use AppState data + final profileImageData = customProfileImageData ?? appState.getProfileImageData; + + // Determine the default image based on gender and age + final String defaultImage; + if (userGender == 1) { + // Male + defaultImage = userAge < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg; + } else { + // Female + defaultImage = userAge < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg; + } + + // Calculate border radius - default to 12.r if not specified and not circular + final effectiveBorderRadius = isCircular ? width / 2 : (borderRadius ?? 12.r); + + // Show uploaded profile image if available + if (profileImageData != null && profileImageData.isNotEmpty) { + try { + final bytes = base64Decode(profileImageData); + final imageWidget = Image.memory( + bytes, + width: width, + height: height, + fit: fit ?? BoxFit.cover, + ); + + return ClipRRect( + borderRadius: BorderRadius.circular(effectiveBorderRadius), + child: imageWidget, + ); + } catch (e) { + // If decoding fails, show default image + return ClipRRect( + borderRadius: BorderRadius.circular(effectiveBorderRadius), + child: Image.asset( + defaultImage, + width: width, + height: height, + fit: fit, + ), + ); + } + } + + // Show default image with rounded corners + return ClipRRect( + borderRadius: BorderRadius.circular(effectiveBorderRadius), + child: Image.asset( + defaultImage, + width: width, + height: height, + fit: fit, + ), + ); + } +} + diff --git a/pubspec.yaml b/pubspec.yaml index 833c5f4c..7f14f57c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -97,6 +97,7 @@ dependencies: intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 huawei_map: ^6.12.0+301 + flutter_image_compress: ^2.3.0 scrollable_positioned_list: ^0.3.8 -- 2.30.2 From fcdc96d944659f7e4f4d38ebbf620e9690f57579 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 9 Apr 2026 14:42:32 +0300 Subject: [PATCH 2/4] no message --- lib/core/app_state.dart | 4 ---- .../my_family/widget/family_cards.dart | 2 +- .../widgets/profile_picture_widget.dart | 18 +++++++++++++++--- lib/widgets/user_avatar_widget.dart | 5 ----- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 1b4a5e4d..85fe9cb2 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -94,22 +94,18 @@ class AppState { String? get getProfileImageData => _profileImageData; set setProfileImageData(String? value) { - print('📸 AppState: Setting profile image data - ${value != null && value.isNotEmpty ? "Has data (${value.length} chars)" : "Empty/null"}'); _profileImageData = value; // Persist to cache if (value != null && value.isNotEmpty) { cacheService.saveString(key: _profileImageKey, value: value); - print('💾 AppState: Profile image saved to cache'); } else { cacheService.remove(key: _profileImageKey); - print('🗑️ AppState: Profile image removed from cache'); } } /// Load profile image from cache on app initialization void _loadProfileImageFromCache() { _profileImageData = cacheService.getString(key: _profileImageKey); - print('📂 AppState: Loaded profile image from cache - ${_profileImageData != null && _profileImageData!.isNotEmpty ? "Has data (${_profileImageData!.length} chars)" : "Empty/null"}'); } /// Clear profile image from cache (e.g., on logout) diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index b3081dc1..60ec335a 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -312,7 +312,7 @@ class _FamilyCardsState extends State { crossAxisCount: 2, crossAxisSpacing: 10.w, mainAxisSpacing: 10.h, - // childAspectRatio: widget.isShowDetails ? 0.56.h : 0.74.h, + childAspectRatio: widget.isShowDetails ? 0.80.h : 0.80.h, // childAspectRatio: 0.7.h, ), padding: EdgeInsets.only(bottom: 20.h), diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index 7cbeb0ea..387907fa 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -32,12 +32,24 @@ class _ProfilePictureWidgetState extends State { @override void initState() { super.initState(); + // Use addPostFrameCallback to ensure widget is built before loading WidgetsBinding.instance.addPostFrameCallback((_) { - // Sync ViewModel with AppState first + if (!mounted) return; + final profileVm = context.read(); - profileVm.syncProfileImageFromAppState(); - _loadProfileImage(); + // Only sync if AppState has data and ViewModel doesn't + if (_appState.getProfileImageData != null && + _appState.getProfileImageData!.isNotEmpty && + (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { + profileVm.syncProfileImageFromAppState(); + } + + // Only load if no data exists in both AppState and ViewModel + if ((_appState.getProfileImageData == null || _appState.getProfileImageData!.isEmpty) && + (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { + _loadProfileImage(); + } }); } diff --git a/lib/widgets/user_avatar_widget.dart b/lib/widgets/user_avatar_widget.dart index 56545a0d..6132401c 100644 --- a/lib/widgets/user_avatar_widget.dart +++ b/lib/widgets/user_avatar_widget.dart @@ -36,11 +36,6 @@ class UserAvatarWidget extends StatelessWidget { // Use custom profile image data if provided, otherwise use AppState data final profileImageData = customProfileImageData ?? appState.getProfileImageData; - // Debug logging - print('🖼️ UserAvatarWidget build - Has profile image: ${profileImageData != null && profileImageData.isNotEmpty}'); - if (profileImageData != null && profileImageData.isNotEmpty) { - print('🖼️ Profile image data length: ${profileImageData.length} characters'); - } // Determine the default image based on gender and age final String defaultImage; -- 2.30.2 From 21c7b05df9aefd950708f202d9c7d7a803821cb9 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 9 Apr 2026 14:54:13 +0300 Subject: [PATCH 3/4] no message --- lib/presentation/home/landing_page.dart | 23 +++++-------------- .../home/widgets/welcome_widget.dart | 9 ++++++++ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index c89727fa..1c4a6b63 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -229,6 +229,8 @@ class _LandingPageState extends State { child: Column( spacing: 16.h, children: [ + + Row( spacing: 8.h, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -236,15 +238,7 @@ class _LandingPageState extends State { appState.isAuthenticated ? WelcomeWidget( onTap: () { - // DialogService dialogService = getIt.get(); - // dialogService.showFamilyBottomSheetWithoutH( - // label: LocaleKeys.familyTitle.tr(context: context), - // message: "", - // isShowManageButton: true, - // onSwitchPress: (FamilyFileResponseModelLists profile) { - // getIt.get().switchFamilyFiles(responseID: profile.responseId, patientID: profile.patientId, phoneNumber: profile.mobileNumber); - // }, - // profiles: getIt.get().patientFamilyFiles); + // ...existing navigation code... Navigator.of(context).push( CustomPageRoute( @@ -265,14 +259,7 @@ 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!)), - // ); + // ...existing commented code... }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, @@ -288,6 +275,8 @@ class _LandingPageState extends State { mainAxisSize: MainAxisSize.min, // spacing: 18.h, children: [ + + Stack(clipBehavior: Clip.none, children: [ if (appState.isAuthenticated) Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h).onPress(() async { diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 3131fbbf..bdd71b53 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -52,6 +52,15 @@ class WelcomeWidget extends StatelessWidget { ), ); }), + Container( + height: 32.h, + width: 1.5.w, + margin: EdgeInsets.only(right: 10.w,left: 10.w), + decoration: BoxDecoration( + color: AppColors.greyTextColor.withOpacity(.3), + borderRadius: BorderRadius.circular(1.r), + ), + ), imageWidget ?? (imageUrl != null ? Image.asset(imageUrl!, width: 40, height: 40) -- 2.30.2 From 1aacda08a34bfe3041ce4c78cbae350bd1c228be Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Thu, 9 Apr 2026 14:57:40 +0300 Subject: [PATCH 4/4] no message --- .../profile_settings/widgets/profile_picture_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index 387907fa..1b63fef3 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -370,7 +370,7 @@ class _ProfilePictureWidgetState extends State { setState(() { _selectedImage = null; // Clear selected image after successful upload }); - Utils.showToast( + print( LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context), ); } -- 2.30.2