From 460e7cdcea7c5846c2b095c3c78a899ff8c15b23 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Tue, 3 Nov 2026 15:41:38 +0300 Subject: [PATCH 1/2] profile picture fixed. --- .../authentication_view_model.dart | 42 ++++--------------- .../profile_picture_view_model.dart | 18 ++++++-- .../profile_settings_view_model.dart | 33 ++++++++++++--- lib/presentation/home/landing_page.dart | 12 ++++++ .../widgets/profile_picture_widget.dart | 27 +++++++----- 5 files changed, 80 insertions(+), 52 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index cb164a0c..04e66039 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -33,6 +33,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_mode import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; @@ -52,7 +53,6 @@ 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'; @@ -1824,6 +1824,7 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setAppAuthToken = ""; _appState.setUserBloodGroup = ""; _appState.clearProfileImageCache(); // Clear profile image cache + GetIt.instance().clearProfileImageCache(); _appState.setSuperUserID = null; _appState.setIsChildLoggedIn = false; _appState.setFamilyFileTokenID = ""; @@ -1877,43 +1878,16 @@ class AuthenticationViewModel extends ChangeNotifier { try { log("Fetching profile image for patient ID: $patientID"); - // IMPORTANT: Clear old cache BEFORE fetching new image - // This ensures old data is removed immediately + // Clear old cache before fetching new image _appState.clearProfileImageCache(); log("🧹 Cleared profile image cache before fetch"); - final profileSettingsRepo = GetIt.instance(); - final result = await profileSettingsRepo.getProfileImage(patientID: patientID); + final profileSettingsVM = GetIt.instance(); + profileSettingsVM.clearProfileImageCache(); - result.fold( - (failure) { - // Silently fail - profile image is optional - log("Failed to fetch profile image: ${failure.message}"); - // Ensure cache is cleared even on failure - _appState.setProfileImageData = null; - }, - (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]['ImageData']; - if (imageData != null && imageData.isNotEmpty) { - // Set new profile image data - this will update cache and notify all listeners - _appState.setProfileImageData = imageData; - log("✅ Profile image loaded and cached successfully for patient: $patientID"); - } else { - log("⚠️ Profile image data is empty for patient: $patientID"); - _appState.setProfileImageData = null; - } - } else { - log("⚠️ Profile image list is empty for patient: $patientID"); - _appState.setProfileImageData = null; - } - } else { - log("⚠️ No profile image data in response for patient: $patientID"); - _appState.setProfileImageData = null; - } - }, + await profileSettingsVM.getProfileImage( + patientID: patientID, + forceRefresh: true, ); } catch (e) { log("❌ Error fetching profile image: $e"); diff --git a/lib/features/profile_picture/profile_picture_view_model.dart b/lib/features/profile_picture/profile_picture_view_model.dart index 59198809..155ed77d 100644 --- a/lib/features/profile_picture/profile_picture_view_model.dart +++ b/lib/features/profile_picture/profile_picture_view_model.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; @@ -105,7 +106,7 @@ class ProfilePictureViewModel extends ChangeNotifier { _cachedImageDataHash = null; _selectedImage = null; - notifyListeners(); + safeNotifyListeners(); // Load new user's profile image _profileSettingsViewModel.getProfileImage( @@ -113,14 +114,25 @@ class ProfilePictureViewModel extends ChangeNotifier { forceRefresh: true, onSuccess: (data) { _tryCacheExistingImage(); - notifyListeners(); + safeNotifyListeners(); }, onError: (error) { - notifyListeners(); + safeNotifyListeners(); }, ); } + /// Safely dispatch notifyListeners to avoid setState() or markNeedsBuild() during build + void safeNotifyListeners() { + if (WidgetsBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { + WidgetsBinding.instance.addPostFrameCallback((_) { + notifyListeners(); + }); + } else { + notifyListeners(); + } + } + /// Load profile image from API void loadProfileImage({bool forceRefresh = false}) { // Check if profile image is already loaded in AppState (skip if forcing refresh) diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index c61665c3..3450432d 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.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/medical_file/medical_file_view_model.dart'; @@ -351,15 +352,21 @@ class ProfileSettingsViewModel extends ChangeNotifier { Function(String)? onError, bool forceRefresh = false, // Add flag to force refresh }) async { + if (isProfileImageLoading && !forceRefresh) { + print('⏳ Profile image is already loading, skipping duplicate call'); + return; + } + print('🌐 Fetching profile image from API for patient: $patientID (forceRefresh: $forceRefresh)'); // IMPORTANT: Always clear old data BEFORE fetching // Do NOT use cached data - always fetch fresh from API profileImageData = null; + GetIt.instance().setProfileImageData = null; isProfileImageLoading = true; profileImageError = null; - notifyListeners(); // Notify to show loading state + safeNotifyListeners(); // Notify to show loading state final result = await profileSettingsRepo.getProfileImage(patientID: patientID); @@ -371,8 +378,9 @@ class ProfileSettingsViewModel extends ChangeNotifier { // Ensure data is cleared on failure profileImageData = null; + GetIt.instance().setProfileImageData = null; - notifyListeners(); + safeNotifyListeners(); if (onError != null) { onError(failure.message); } @@ -387,32 +395,47 @@ class ProfileSettingsViewModel extends ChangeNotifier { profileImageData = imageList[0]['ImageData']; if (profileImageData != null && profileImageData!.isNotEmpty) { - // ONLY store in ViewModel - AppState will be updated separately by auth flow + GetIt.instance().setProfileImageData = profileImageData; print('✅ Profile image loaded from API (length: ${profileImageData!.length})'); } else { profileImageData = null; + GetIt.instance().setProfileImageData = null; print('⚠️ Profile image data is empty in API response'); } } else { profileImageData = null; + GetIt.instance().setProfileImageData = null; print('⚠️ Profile image list is empty in API response'); } } else { profileImageData = null; + GetIt.instance().setProfileImageData = null; print('⚠️ No profile image data in API response'); } - notifyListeners(); // Notify to update UI with new/null data + safeNotifyListeners(); // Notify to update UI with new/null data onSuccess?.call(response.data); }, ); } + /// Safely dispatch notifyListeners to avoid setState() or markNeedsBuild() during build + void safeNotifyListeners() { + if (WidgetsBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { + WidgetsBinding.instance.addPostFrameCallback((_) { + notifyListeners(); + }); + } else { + notifyListeners(); + } + } + /// Clear profile image data from ViewModel void clearProfileImageCache() { print('🧹 Clearing profile image from ViewModel'); profileImageData = null; - notifyListeners(); + GetIt.instance().setProfileImageData = null; + safeNotifyListeners(); } /// Sync profile image data from AppState - DISABLED diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index f475c022..4f128e2a 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -150,9 +150,21 @@ class _LandingPageState extends State { await authVM.handleNewImeiRegistration(isNavigate: false); } + if (!mounted) return; + if (appState.isAuthenticated) { Clarity.setCustomUserId(appState.getAuthenticatedUser()!.patientId.toString()); + final profileSettingsVM = context.read(); + if (profileSettingsVM.profileImageData == null && + (appState.getProfileImageData == null || appState.getProfileImageData!.isEmpty) && + !profileSettingsVM.isProfileImageLoading) { + final patientID = appState.getAuthenticatedUser()?.patientId; + if (patientID != null) { + profileSettingsVM.getProfileImage(patientID: patientID); + } + } + // habibWalletVM.initHabibWalletProvider(); // Wallet & Lakum data are fetched lazily when the user unhides the values. // habibWalletVM.getPatientBalanceAmount(); diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index 5fe71e1e..32601299 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -30,11 +30,12 @@ class _ProfilePictureWidgetState extends State { void initState() { super.initState(); - // Initialize view model + // Initialize view model synchronously so currentPatientId is immediately set + final profilePictureViewModel = context.read(); + profilePictureViewModel.initialize(); + WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - final profilePictureViewModel = context.read(); - profilePictureViewModel.initialize(); profilePictureViewModel.triggerInitialLoad(); }); } @@ -42,18 +43,24 @@ class _ProfilePictureWidgetState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - // Check for user switch - final profilePictureViewModel = context.read(); - if (profilePictureViewModel.isInitialLoadTriggered) { - profilePictureViewModel.checkForUserSwitch(); - } + // Check for user switch safely after frame build to avoid markNeedsBuild during build + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final profilePictureViewModel = context.read(); + if (profilePictureViewModel.isInitialLoadTriggered) { + profilePictureViewModel.checkForUserSwitch(); + } + }); } @override void didUpdateWidget(ProfilePictureWidget oldWidget) { super.didUpdateWidget(oldWidget); - // Check for user switch - context.read().checkForUserSwitch(); + // Check for user switch safely after frame build to avoid markNeedsBuild during build + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + context.read().checkForUserSwitch(); + }); } void _pickImage() { From a2645f7309257956ceb5360ad23c8b0f77c3ecc8 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Sun, 8 Nov 2026 16:06:09 +0300 Subject: [PATCH 2/2] no message --- .../book_appointments_view_model.dart | 11 +- .../book_appointment/select_clinic_page.dart | 70 ++--- .../medical_file/medical_file_page.dart | 124 ++++++--- lib/widgets/appbar/collapsing_list_view.dart | 250 +++++++++++------- 4 files changed, 279 insertions(+), 176 deletions(-) diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 5865a829..dc48f041 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -369,20 +369,23 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void filterClinics(String? query) { - if (query!.isEmpty) { + if (query == null || query.trim().isEmpty) { _filteredClinicsList = List.from(clinicsList); showSortFilterButtons = false; } else { // Normalize the search query for better Arabic text matching - String normalizedQuery = Utils.normalizeArabicText(query.toLowerCase()); + String normalizedQuery = Utils.normalizeArabicText(query.trim().toLowerCase()); _filteredClinicsList = clinicsList.where((clinic) { String clinicName = clinic.clinicDescription?.toLowerCase() ?? ''; + String clinicNameN = clinic.clinicDescriptionN?.toLowerCase() ?? ''; String normalizedClinicName = Utils.normalizeArabicText(clinicName); - return normalizedClinicName.contains(normalizedQuery); + String normalizedClinicNameN = Utils.normalizeArabicText(clinicNameN); + return normalizedClinicName.contains(normalizedQuery) || + normalizedClinicNameN.contains(normalizedQuery); }).toList(); - showSortFilterButtons = query.length >= 3; + showSortFilterButtons = query.trim().length >= 3; } notifyListeners(); } diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index c421561d..9db7c0e4 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -115,44 +115,52 @@ class _SelectClinicPageState extends State { backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: bookAppointmentsViewModel.isLiveCareSchedule ? LocaleKeys.selectLiveCareClinic.tr(context: context) : LocaleKeys.selectClinic.tr(context: context), + stickyWidgetHeight: 56.h, + stickyWidget: Padding( + padding: EdgeInsets.fromLTRB(24.h, 0, 24.h, 14.h), + child: Consumer( + builder: (context, _, __) { + return TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.clinicName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ); + }, + ), + ), child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), child: Consumer(builder: (context, bookAppointmentsVM, child) { return Column( children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.clinicName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, - ), - ), bookAppointmentsVM.isLiveCareSchedule ? ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 12.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isClinicsListLoading @@ -199,7 +207,7 @@ class _SelectClinicPageState extends State { separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ) : ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 12.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index bd69fb7e..27ce63ef 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -60,6 +60,7 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.da import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_details_page.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -429,13 +430,6 @@ class _MedicalFilePageState extends State { }, children: _buildVitalSignPages( vitalSign: hmgServicesVM.vitalSignList.first, - onTap: () { - Navigator.of(context).push( - CustomPageRoute( - page: VitalSignPage(), - ), - ); - }, currentPageIndex: hmgServicesVM.vitalSignCurrentPage, ), ), @@ -1496,10 +1490,20 @@ class _MedicalFilePageState extends State { ); } + void _openVitalSignDetails(VitalSignDetailsArgs args, VitalSignResModel? targetVitalSign) { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignDetailsPage( + args: args, + selectedVitalSign: targetVitalSign, + ), + ), + ); + } + // Build pages with 2 cards each List _buildVitalSignPages({ required VitalSignResModel vitalSign, - required VoidCallback onTap, required int currentPageIndex, }) { return [ @@ -1515,8 +1519,15 @@ class _MedicalFilePageState extends State { value: _formatValue(vitalSign.bodyMassIndex), unit: '', status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, - // status: "Overweight", - onTap: onTap, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.bmi, + title: LocaleKeys.bmi.tr(context: context), + icon: AppAssets.bmiVital, + unit: '', + ), + vitalSign, + ), ), ), SizedBox(width: 12.w), @@ -1527,7 +1538,15 @@ class _MedicalFilePageState extends State { value: _formatValue(vitalSign.heightCm), unit: LocaleKeys.cm.tr(), status: null, - onTap: onTap, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.height, + title: LocaleKeys.height.tr(context: context), + icon: AppAssets.heightVital, + unit: 'cm', + ), + vitalSign, + ), ), ), ], @@ -1535,39 +1554,64 @@ class _MedicalFilePageState extends State { ), // Page 2: Weight + Blood Pressure Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.weightVital, - label: LocaleKeys.weight.tr(context: context), - value: _formatValue(vitalSign.weightKg), - unit: LocaleKeys.kg.tr(), - status: vitalSign.weightKg != null ? _getWeightStatus(vitalSign.bodyMassIndex) : null, - onTap: onTap, + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: LocaleKeys.weight.tr(context: context), + value: _formatValue(vitalSign.weightKg), + unit: LocaleKeys.kg.tr(), + status: vitalSign.weightKg != null ? _getWeightStatus(vitalSign.bodyMassIndex) : null, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.weight, + title: LocaleKeys.weight.tr(context: context), + icon: AppAssets.weightVital, + unit: 'kg', + ), + vitalSign, ), ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bloodPressure, - label: LocaleKeys.bloodPressure.tr(context: context), - value: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0) - ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" - : '--', - unit: '', - status: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0) - ? _getBloodPressureStatus( - systolic: vitalSign.bloodPressureHigher, - diastolic: vitalSign.bloodPressureLower, - ) - : null, - onTap: onTap, + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: LocaleKeys.bloodPressure.tr(context: context), + value: (vitalSign.bloodPressureLower != null && + vitalSign.bloodPressureHigher != null && + vitalSign.bloodPressureLower != 0 && + vitalSign.bloodPressureHigher != 0) + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: (vitalSign.bloodPressureLower != null && + vitalSign.bloodPressureHigher != null && + vitalSign.bloodPressureLower != 0 && + vitalSign.bloodPressureHigher != 0) + ? _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ) + : null, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.bloodPressure, + title: LocaleKeys.bloodPressure.tr(context: context), + icon: AppAssets.bloodPressure, + unit: 'mmHg', + low: 90, + high: 140, + ), + vitalSign, ), ), - ], - )), + ), + ], + ), + ), ]; } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index 0c677002..40ca8a98 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -13,28 +13,30 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import '../../core/dependencies.dart'; -class CollapsingListView extends StatelessWidget { +class CollapsingListView extends StatefulWidget { final String title; - Widget? child; - VoidCallback? search; - VoidCallback? report; - VoidCallback? logout; - VoidCallback? aiOverview; - VoidCallback? history; - VoidCallback? instructions; - VoidCallback? requests; - VoidCallback? sendEmail; - VoidCallback? doctorResponse; - VoidCallback? downloadReport; - VoidCallback? viewImage; - VoidCallback? location; - VoidCallback? downloadInvoice; - Widget? bottomChild; - Widget? trailing; - bool isClose; - bool isLeading; - VoidCallback? leadingCallback; - ScrollPhysics? physics; + final Widget? child; + final VoidCallback? search; + final VoidCallback? report; + final VoidCallback? logout; + final VoidCallback? aiOverview; + final VoidCallback? history; + final VoidCallback? instructions; + final VoidCallback? requests; + final VoidCallback? sendEmail; + final VoidCallback? doctorResponse; + final VoidCallback? downloadReport; + final VoidCallback? viewImage; + final VoidCallback? location; + final VoidCallback? downloadInvoice; + final Widget? bottomChild; + final Widget? trailing; + final bool isClose; + final bool isLeading; + final VoidCallback? leadingCallback; + final ScrollPhysics? physics; + final Widget? stickyWidget; + final double? stickyWidgetHeight; CollapsingListView({ super.key, @@ -59,9 +61,28 @@ class CollapsingListView extends StatelessWidget { this.trailing, this.leadingCallback, this.physics, + this.stickyWidget, + this.stickyWidgetHeight, }); - final ScrollController _controller = ScrollController(); + @override + State createState() => _CollapsingListViewState(); +} + +class _CollapsingListViewState extends State { + late final ScrollController _controller; + + @override + void initState() { + super.initState(); + _controller = ScrollController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -71,82 +92,66 @@ class CollapsingListView extends StatelessWidget { body: Column( children: [ CustomScrollView( - physics: physics, + physics: widget.physics, controller: _controller, slivers: [ SliverAppBar( - automaticallyImplyLeading: isLeading, - pinned: true, - toolbarHeight: 40.h, - leadingWidth: isLeading ? 40.w + 32.w : double.infinity, - systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), - surfaceTintColor: Colors.transparent, - backgroundColor: AppColors.bgScaffoldColor, - bottom: - // isLeading - // ? - ScrollAnimatedTitle( - title: title, - showBack: true, - controller: _controller, - search: search, - report: report, - logout: logout, - history: history, - instructions: instructions, - requests: requests, - sendEmail: sendEmail, - doctorResponse: doctorResponse, - downloadReport: downloadReport, - viewImage: viewImage, - location: location, - downloadInvoice: downloadInvoice, - bottomChild: bottomChild, - trailing: trailing, - aiOverview: aiOverview, - ), - // : null, - leading: Visibility( - visible: isLeading, - child: Container( - height: 32.h, - width: 32.w, - alignment: appState.isArabic() ? Alignment.centerRight : Alignment.centerLeft, - margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), - child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), - ), - ).onPress(() { - if (leadingCallback != null) { - leadingCallback!(); - } else { - context.pop(); - } - }), - ) - // : ScrollAnimatedTitle( - // title: title, - // showBack: false, - // controller: _controller, - // search: search, - // report: report, - // logout: logout, - // history: history, - // instructions: instructions, - // requests: requests, - // sendEmail: sendEmail, - // bottomChild: bottomChild, - // trailing: trailing, - // ), - ), - if (child != null) + automaticallyImplyLeading: widget.isLeading, + pinned: true, + toolbarHeight: 40.h, + leadingWidth: widget.isLeading ? 40.w + 32.w : double.infinity, + systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), + surfaceTintColor: Colors.transparent, + backgroundColor: AppColors.bgScaffoldColor, + bottom: ScrollAnimatedTitle( + title: widget.title, + showBack: true, + controller: _controller, + search: widget.search, + report: widget.report, + logout: widget.logout, + history: widget.history, + instructions: widget.instructions, + requests: widget.requests, + sendEmail: widget.sendEmail, + doctorResponse: widget.doctorResponse, + downloadReport: widget.downloadReport, + viewImage: widget.viewImage, + location: widget.location, + downloadInvoice: widget.downloadInvoice, + bottomChild: widget.bottomChild, + trailing: widget.trailing, + aiOverview: widget.aiOverview, + stickyWidget: widget.stickyWidget, + stickyWidgetHeight: widget.stickyWidgetHeight, + ), + leading: Visibility( + visible: widget.isLeading, + child: Container( + height: 32.h, + width: 32.w, + alignment: appState.isArabic() ? Alignment.centerRight : Alignment.centerLeft, + margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), + child: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), + ), + ).onPress(() { + if (widget.leadingCallback != null) { + widget.leadingCallback!(); + } else { + context.pop(); + } + }), + ), + ), + if (widget.child != null) SliverToBoxAdapter( - child: child!, + child: widget.child!, ), ], ).expanded, - if (bottomChild != null) bottomChild! + if (widget.bottomChild != null) widget.bottomChild! ], ), ); @@ -216,6 +221,8 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget VoidCallback? downloadInvoice; Widget? bottomChild; Widget? trailing; + Widget? stickyWidget; + double? stickyWidgetHeight; ScrollAnimatedTitle({ super.key, @@ -237,10 +244,12 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget this.downloadInvoice, this.bottomChild, this.trailing, + this.stickyWidget, + this.stickyWidgetHeight, }); @override - Size get preferredSize => const Size.fromHeight(56.0); + Size get preferredSize => Size.fromHeight(56.0 + (stickyWidget != null ? (stickyWidgetHeight ?? 58.h) : 0)); @override State createState() => _ScrollAnimatedTitleState(); @@ -254,6 +263,7 @@ class _ScrollAnimatedTitleState extends State { static const double _minHeight = 56; double _fontSize = _maxFont; + double t = 1.0; @override void initState() { @@ -261,33 +271,53 @@ class _ScrollAnimatedTitleState extends State { widget.controller.addListener(_onScroll); } + @override + void didUpdateWidget(covariant ScrollAnimatedTitle oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_onScroll); + widget.controller.addListener(_onScroll); + _onScroll(); + } + } + @override void dispose() { widget.controller.removeListener(_onScroll); super.dispose(); } - double t = 1.0; - void _onScroll() { + if (!widget.controller.hasClients) return; final double offset = widget.controller.offset; // control animation range const double range = 120; - final double t = (1 - (offset / range)).clamp(0.0, 1.0); - this.t = t; - setState(() { - _fontSize = _minFont + (_maxFont - _minFont) * t; - }); + final double newT = (1 - (offset / range)).clamp(0.0, 1.0); + if (newT != t) { + setState(() { + t = newT; + _fontSize = _minFont + (_maxFont - _minFont) * t; + }); + } } @override Widget build(BuildContext context) { final isRtl = Directionality.of(context) == TextDirection.RTL; - return Container( + + final double titleHeight = widget.stickyWidget != null + ? (30.h + (44.h - 30.h) * t) + : 56.h; + + final double topGap = widget.stickyWidget != null + ? (2.h + (10.h - 2.h) * t) + : 0.0; + + final titleWidget = Container( // height: (widget.preferredSize.height - _fontSize / 2).h, - height: 56.h, + height: titleHeight, alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Row( @@ -322,6 +352,24 @@ class _ScrollAnimatedTitleState extends State { ], ), ); + + if (widget.stickyWidget == null) { + return titleWidget; + } + + return Container( + color: AppColors.bgScaffoldColor, + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + titleWidget, + SizedBox(height: topGap), + widget.stickyWidget!, + ], + ), + ); } Widget actionButton(BuildContext context, double t, {required String title, required String icon, bool isAiButton = false}) {