From c19451a6b6da1c270ae2affb7d41b6cae347d9a1 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Tue, 14 Apr 2026 11:32:11 +0300 Subject: [PATCH] profile picture fix --- .../health_trackers_view_model.dart | 6 + .../add_health_tracker_entry_page.dart | 2 +- .../profile_settings/profile_settings.dart | 20 +-- .../widgets/profile_picture_widget.dart | 149 +++++++++++------- 4 files changed, 112 insertions(+), 65 deletions(-) diff --git a/lib/features/health_trackers/health_trackers_view_model.dart b/lib/features/health_trackers/health_trackers_view_model.dart index 8f7e0ffa..8fcbee01 100644 --- a/lib/features/health_trackers/health_trackers_view_model.dart +++ b/lib/features/health_trackers/health_trackers_view_model.dart @@ -2,6 +2,8 @@ import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.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/features/health_trackers/health_trackers_repo.dart'; import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/blood_pressure_result.dart'; @@ -76,6 +78,10 @@ class HealthTrackersViewModel extends ChangeNotifier { "آخر", ]; + List getBloodSugarTimeList(){ + return getIt.get().isArabic()?bloodSugarMeasureTimeArList:bloodSugarMeasureTimeEnList; + } + // Setters with notification void setBloodSugarMeasureTime(String duration) async { _selectedBloodSugarMeasureTime = duration; diff --git a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart index 2d0356c3..0305eebf 100644 --- a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart +++ b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart @@ -251,7 +251,7 @@ class _AddHealthTrackerEntryPageState extends State { _showSelectionBottomSheet( context: context, title: LocaleKeys.selectMeasureTime.tr(context: context), - items: viewModel.bloodSugarMeasureTimeEnList, + items: viewModel.getBloodSugarTimeList(), selectedValue: viewModel.selectedBloodSugarMeasureTime, onSelected: viewModel.setBloodSugarMeasureTime, useUpperCase: false, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 750ea8c4..be7ddb54 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -450,16 +450,16 @@ class _FamilyCardWidgetState extends State { }); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Also sync when dependencies change (e.g., after user switch) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _syncProfileImageFromAppState(); - } - }); - } + // @override + // void didChangeDependencies() { + // super.didChangeDependencies(); + // // Also sync when dependencies change (e.g., after user switch) + // WidgetsBinding.instance.addPostFrameCallback((_) { + // if (mounted) { + // _syncProfileImageFromAppState(); + // } + // }); + // } void _syncProfileImageFromAppState() { try { diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index c9c5e7b1..e7abcb12 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.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'; @@ -29,16 +31,24 @@ class _ProfilePictureWidgetState extends State { final AppState _appState = getIt.get(); File? _selectedImage; int? _currentPatientId; + bool _isInitialLoadTriggered = false; + + /// Cache decoded image bytes to avoid decoding base64 on every rebuild + Uint8List? _cachedImageBytes; + String? _cachedImageDataHash; @override void initState() { super.initState(); _currentPatientId = _appState.getAuthenticatedUser()?.patientId; + // Pre-cache existing image data if available (prevents blink from default → loaded) + _tryCacheExistingImage(); // Use addPostFrameCallback to ensure widget is built before loading WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; + if (!mounted || _isInitialLoadTriggered) return; + _isInitialLoadTriggered = true; final patientID = _appState.getAuthenticatedUser()?.patientId; @@ -47,16 +57,19 @@ class _ProfilePictureWidgetState extends State { return; } - // Always load fresh data from API - do NOT use AppState cache + // Load fresh data from API print('📥 Loading fresh profile image from API for patient: $patientID'); - _loadProfileImage(forceRefresh: true); + _loadProfileImage(forceRefresh: false); }); } @override void didChangeDependencies() { super.didChangeDependencies(); - _checkAndUpdateUserImage(); + // Only check for user switch, NOT on initial load (initState handles that) + if (_isInitialLoadTriggered) { + _checkAndUpdateUserImage(); + } } @override @@ -65,6 +78,20 @@ class _ProfilePictureWidgetState extends State { _checkAndUpdateUserImage(); } + /// Pre-cache already-loaded image bytes so we don't flash default avatar + void _tryCacheExistingImage() { + final imageData = _appState.getProfileImageData; + if (imageData != null && imageData.isNotEmpty) { + try { + _cachedImageBytes = base64Decode(imageData); + _cachedImageDataHash = '${imageData.length}_${imageData.hashCode}'; + } catch (_) { + _cachedImageBytes = null; + _cachedImageDataHash = null; + } + } + } + void _checkAndUpdateUserImage() { // Check if the authenticated user has changed (family member switch) final currentPatientId = _appState.getAuthenticatedUser()?.patientId; @@ -88,6 +115,10 @@ class _ProfilePictureWidgetState extends State { // Then clear ViewModel cache profileVm.clearProfileImageCache(); + // Clear local decoded bytes cache + _cachedImageBytes = null; + _cachedImageDataHash = null; + // Force rebuild to show default avatar immediately if (mounted) { setState(() { @@ -103,6 +134,7 @@ class _ProfilePictureWidgetState extends State { onSuccess: (data) { print('✅ Profile image loaded successfully for user: $currentPatientId'); if (mounted) { + _tryCacheExistingImage(); setState(() {}); // Force rebuild to show new data } }, @@ -136,7 +168,10 @@ class _ProfilePictureWidgetState extends State { forceRefresh: forceRefresh, onSuccess: (data) { print('✅ Profile image loaded successfully'); - // Image loaded successfully + if (mounted) { + _tryCacheExistingImage(); + setState(() {}); // Rebuild with new cached bytes + } }, onError: (error) { print('❌ Error loading profile image: $error'); @@ -437,6 +472,8 @@ class _ProfilePictureWidgetState extends State { imageData: base64String, onSuccess: (data) { if (mounted) { + // Update cached bytes immediately from the uploaded data + _tryCacheExistingImage(); setState(() { _selectedImage = null; // Clear selected image after successful upload }); @@ -481,80 +518,84 @@ class _ProfilePictureWidgetState extends State { ); } - // // IMPORTANT: Verify the cached image belongs to the current user - // // If _currentPatientId doesn't match currentPatientId, don't show cached image - // if (currentPatientId != _currentPatientId) { - // print('⚠️ Patient ID mismatch - showing default avatar (current: $currentPatientId, cached: $_currentPatientId)'); - // return Image.asset( - // defaultImage, - // width: 136.w, - // height: 136.h, - // ); - // } - - // IMPORTANT: Use ONLY ViewModel data, DO NOT fallback to AppState - // This prevents showing stale cached data when switching users - final String? imageData = profileVm.profileImageData; + // Use cached decoded bytes — update cache if source data changed + final String? imageData = GetIt.instance().getProfileImageData; + final String? currentHash = (imageData != null && imageData.isNotEmpty) + ? '${imageData.length}_${imageData.hashCode}' + : null; - print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: $currentPatientId (ViewModel ONLY)'); - - // Show uploaded image if available - if (imageData != null && imageData.isNotEmpty) { + // Re-decode only if the underlying data actually changed + if (currentHash != null && currentHash != _cachedImageDataHash) { try { - final bytes = base64Decode(imageData); - return ClipOval( - child: Image.memory( - bytes, - width: 136.w, - height: 136.h, - fit: BoxFit.cover, - ), - ); + _cachedImageBytes = base64Decode(imageData!); + _cachedImageDataHash = currentHash; } catch (e) { print('❌ Error decoding profile image: $e'); - // If decoding fails, show default image - return Image.asset( - defaultImage, + _cachedImageBytes = null; + _cachedImageDataHash = null; + } + } else if (currentHash == null) { + _cachedImageBytes = null; + _cachedImageDataHash = null; + } + + // Show cached decoded image if available + if (_cachedImageBytes != null) { + return ClipOval( + child: Image.memory( + _cachedImageBytes!, + key: ValueKey('profile_$currentPatientId'), width: 136.w, height: 136.h, - ); - } + fit: BoxFit.cover, + gaplessPlayback: true, // Prevents blink during image rebuild + ), + ); } // Show default image (no image data or user has no uploaded image) print('📷 Showing default avatar for user $currentPatientId'); return Image.asset( defaultImage, + key: ValueKey('default_$currentPatientId'), width: 136.w, height: 136.h, + gaplessPlayback: true, ); } @override Widget build(BuildContext context) { - // Check for user change on every build (handles navigation scenarios) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _checkAndUpdateUserImage(); - } - }); + // Removed addPostFrameCallback from build() — it was causing redundant + // _checkAndUpdateUserImage calls on every single rebuild. + // didChangeDependencies + didUpdateWidget already handle user switches. return Consumer( builder: (context, profileVm, _) { + // If we already have cached bytes, show the image even while "loading" + // to prevent the shimmer→image blink on page open + final bool showShimmer = profileVm.isProfileImageLoading && + _cachedImageBytes == null && + _selectedImage == null; + 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), + // Profile Image — use AnimatedSwitcher to smooth transition + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: showShimmer + ? Container( + key: const ValueKey('shimmer'), + 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( -- 2.30.2