Merge pull request 'dev_sultan' (#404) from dev_sultan into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/404
haroon_dev
Haroon6138 1 day ago
commit 033a4cbfe1

@ -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';
@ -1828,6 +1828,7 @@ class AuthenticationViewModel extends ChangeNotifier {
_appState.setAppAuthToken = "";
_appState.setUserBloodGroup = "";
_appState.clearProfileImageCache(); // Clear profile image cache
GetIt.instance<ProfileSettingsViewModel>().clearProfileImageCache();
_appState.setSuperUserID = null;
_appState.setIsChildLoggedIn = false;
_appState.setFamilyFileTokenID = "";
@ -1881,43 +1882,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<ProfileSettingsRepo>();
final result = await profileSettingsRepo.getProfileImage(patientID: patientID);
final profileSettingsVM = GetIt.instance<ProfileSettingsViewModel>();
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");

@ -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();
}

@ -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)

@ -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<AppState>().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<AppState>().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<AppState>().setProfileImageData = profileImageData;
print('✅ Profile image loaded from API (length: ${profileImageData!.length})');
} else {
profileImageData = null;
GetIt.instance<AppState>().setProfileImageData = null;
print('⚠️ Profile image data is empty in API response');
}
} else {
profileImageData = null;
GetIt.instance<AppState>().setProfileImageData = null;
print('⚠️ Profile image list is empty in API response');
}
} else {
profileImageData = null;
GetIt.instance<AppState>().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<AppState>().setProfileImageData = null;
safeNotifyListeners();
}
/// Sync profile image data from AppState - DISABLED

@ -115,44 +115,52 @@ class _SelectClinicPageState extends State<SelectClinicPage> {
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<BookAppointmentsViewModel>(
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<BookAppointmentsViewModel>(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<SelectClinicPage> {
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,

@ -154,9 +154,21 @@ class _LandingPageState extends State<LandingPage> {
await authVM.handleNewImeiRegistration(isNavigate: false);
}
if (!mounted) return;
if (appState.isAuthenticated) {
Clarity.setCustomUserId(appState.getAuthenticatedUser()!.patientId.toString());
final profileSettingsVM = context.read<ProfileSettingsViewModel>();
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();

@ -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<MedicalFilePage> {
},
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<MedicalFilePage> {
);
}
void _openVitalSignDetails(VitalSignDetailsArgs args, VitalSignResModel? targetVitalSign) {
Navigator.of(context).push(
CustomPageRoute(
page: VitalSignDetailsPage(
args: args,
selectedVitalSign: targetVitalSign,
),
),
);
}
// Build pages with 2 cards each
List<Widget> _buildVitalSignPages({
required VitalSignResModel vitalSign,
required VoidCallback onTap,
required int currentPageIndex,
}) {
return [
@ -1515,8 +1519,15 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
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<MedicalFilePage> {
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<MedicalFilePage> {
),
// 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,
),
),
],
)),
),
],
),
),
];
}

@ -30,11 +30,12 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
void initState() {
super.initState();
// Initialize view model
// Initialize view model synchronously so currentPatientId is immediately set
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
profilePictureViewModel.initialize();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
profilePictureViewModel.initialize();
profilePictureViewModel.triggerInitialLoad();
});
}
@ -42,18 +43,24 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check for user switch
final profilePictureViewModel = context.read<ProfilePictureViewModel>();
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<ProfilePictureViewModel>();
if (profilePictureViewModel.isInitialLoadTriggered) {
profilePictureViewModel.checkForUserSwitch();
}
});
}
@override
void didUpdateWidget(ProfilePictureWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Check for user switch
context.read<ProfilePictureViewModel>().checkForUserSwitch();
// Check for user switch safely after frame build to avoid markNeedsBuild during build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<ProfilePictureViewModel>().checkForUserSwitch();
});
}
void _pickImage() {

@ -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<CollapsingListView> createState() => _CollapsingListViewState();
}
class _CollapsingListViewState extends State<CollapsingListView> {
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<ScrollAnimatedTitle> createState() => _ScrollAnimatedTitleState();
@ -254,6 +263,7 @@ class _ScrollAnimatedTitleState extends State<ScrollAnimatedTitle> {
static const double _minHeight = 56;
double _fontSize = _maxFont;
double t = 1.0;
@override
void initState() {
@ -261,33 +271,53 @@ class _ScrollAnimatedTitleState extends State<ScrollAnimatedTitle> {
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<ScrollAnimatedTitle> {
],
),
);
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}) {

Loading…
Cancel
Save