|
|
|
@ -1,8 +1,10 @@
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
import 'package:flutter/material.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_assets.dart';
|
|
|
|
import 'package:hmg_patient_app_new/core/app_export.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/app_state.dart';
|
|
|
|
@ -29,16 +31,24 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
final AppState _appState = getIt.get<AppState>();
|
|
|
|
final AppState _appState = getIt.get<AppState>();
|
|
|
|
File? _selectedImage;
|
|
|
|
File? _selectedImage;
|
|
|
|
int? _currentPatientId;
|
|
|
|
int? _currentPatientId;
|
|
|
|
|
|
|
|
bool _isInitialLoadTriggered = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Cache decoded image bytes to avoid decoding base64 on every rebuild
|
|
|
|
|
|
|
|
Uint8List? _cachedImageBytes;
|
|
|
|
|
|
|
|
String? _cachedImageDataHash;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
super.initState();
|
|
|
|
_currentPatientId = _appState.getAuthenticatedUser()?.patientId;
|
|
|
|
_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
|
|
|
|
// Use addPostFrameCallback to ensure widget is built before loading
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
if (!mounted) return;
|
|
|
|
if (!mounted || _isInitialLoadTriggered) return;
|
|
|
|
|
|
|
|
_isInitialLoadTriggered = true;
|
|
|
|
|
|
|
|
|
|
|
|
final patientID = _appState.getAuthenticatedUser()?.patientId;
|
|
|
|
final patientID = _appState.getAuthenticatedUser()?.patientId;
|
|
|
|
|
|
|
|
|
|
|
|
@ -47,16 +57,19 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
return;
|
|
|
|
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');
|
|
|
|
print('📥 Loading fresh profile image from API for patient: $patientID');
|
|
|
|
_loadProfileImage(forceRefresh: true);
|
|
|
|
_loadProfileImage(forceRefresh: false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
void didChangeDependencies() {
|
|
|
|
void didChangeDependencies() {
|
|
|
|
super.didChangeDependencies();
|
|
|
|
super.didChangeDependencies();
|
|
|
|
_checkAndUpdateUserImage();
|
|
|
|
// Only check for user switch, NOT on initial load (initState handles that)
|
|
|
|
|
|
|
|
if (_isInitialLoadTriggered) {
|
|
|
|
|
|
|
|
_checkAndUpdateUserImage();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
@ -65,6 +78,20 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
_checkAndUpdateUserImage();
|
|
|
|
_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() {
|
|
|
|
void _checkAndUpdateUserImage() {
|
|
|
|
// Check if the authenticated user has changed (family member switch)
|
|
|
|
// Check if the authenticated user has changed (family member switch)
|
|
|
|
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
|
|
|
|
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
|
|
|
|
@ -88,6 +115,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
// Then clear ViewModel cache
|
|
|
|
// Then clear ViewModel cache
|
|
|
|
profileVm.clearProfileImageCache();
|
|
|
|
profileVm.clearProfileImageCache();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Clear local decoded bytes cache
|
|
|
|
|
|
|
|
_cachedImageBytes = null;
|
|
|
|
|
|
|
|
_cachedImageDataHash = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Force rebuild to show default avatar immediately
|
|
|
|
// Force rebuild to show default avatar immediately
|
|
|
|
if (mounted) {
|
|
|
|
if (mounted) {
|
|
|
|
setState(() {
|
|
|
|
setState(() {
|
|
|
|
@ -103,6 +134,7 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
onSuccess: (data) {
|
|
|
|
onSuccess: (data) {
|
|
|
|
print('✅ Profile image loaded successfully for user: $currentPatientId');
|
|
|
|
print('✅ Profile image loaded successfully for user: $currentPatientId');
|
|
|
|
if (mounted) {
|
|
|
|
if (mounted) {
|
|
|
|
|
|
|
|
_tryCacheExistingImage();
|
|
|
|
setState(() {}); // Force rebuild to show new data
|
|
|
|
setState(() {}); // Force rebuild to show new data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -136,7 +168,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
forceRefresh: forceRefresh,
|
|
|
|
forceRefresh: forceRefresh,
|
|
|
|
onSuccess: (data) {
|
|
|
|
onSuccess: (data) {
|
|
|
|
print('✅ Profile image loaded successfully');
|
|
|
|
print('✅ Profile image loaded successfully');
|
|
|
|
// Image loaded successfully
|
|
|
|
if (mounted) {
|
|
|
|
|
|
|
|
_tryCacheExistingImage();
|
|
|
|
|
|
|
|
setState(() {}); // Rebuild with new cached bytes
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
onError: (error) {
|
|
|
|
onError: (error) {
|
|
|
|
print('❌ Error loading profile image: $error');
|
|
|
|
print('❌ Error loading profile image: $error');
|
|
|
|
@ -437,6 +472,8 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
imageData: base64String,
|
|
|
|
imageData: base64String,
|
|
|
|
onSuccess: (data) {
|
|
|
|
onSuccess: (data) {
|
|
|
|
if (mounted) {
|
|
|
|
if (mounted) {
|
|
|
|
|
|
|
|
// Update cached bytes immediately from the uploaded data
|
|
|
|
|
|
|
|
_tryCacheExistingImage();
|
|
|
|
setState(() {
|
|
|
|
setState(() {
|
|
|
|
_selectedImage = null; // Clear selected image after successful upload
|
|
|
|
_selectedImage = null; // Clear selected image after successful upload
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -481,80 +518,84 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// // IMPORTANT: Verify the cached image belongs to the current user
|
|
|
|
// Use cached decoded bytes — update cache if source data changed
|
|
|
|
// // If _currentPatientId doesn't match currentPatientId, don't show cached image
|
|
|
|
final String? imageData = GetIt.instance<AppState>().getProfileImageData;
|
|
|
|
// if (currentPatientId != _currentPatientId) {
|
|
|
|
final String? currentHash = (imageData != null && imageData.isNotEmpty)
|
|
|
|
// print('⚠️ Patient ID mismatch - showing default avatar (current: $currentPatientId, cached: $_currentPatientId)');
|
|
|
|
? '${imageData.length}_${imageData.hashCode}'
|
|
|
|
// return Image.asset(
|
|
|
|
: null;
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: $currentPatientId (ViewModel ONLY)');
|
|
|
|
// Re-decode only if the underlying data actually changed
|
|
|
|
|
|
|
|
if (currentHash != null && currentHash != _cachedImageDataHash) {
|
|
|
|
// Show uploaded image if available
|
|
|
|
|
|
|
|
if (imageData != null && imageData.isNotEmpty) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
final bytes = base64Decode(imageData);
|
|
|
|
_cachedImageBytes = base64Decode(imageData!);
|
|
|
|
return ClipOval(
|
|
|
|
_cachedImageDataHash = currentHash;
|
|
|
|
child: Image.memory(
|
|
|
|
|
|
|
|
bytes,
|
|
|
|
|
|
|
|
width: 136.w,
|
|
|
|
|
|
|
|
height: 136.h,
|
|
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
print('❌ Error decoding profile image: $e');
|
|
|
|
print('❌ Error decoding profile image: $e');
|
|
|
|
// If decoding fails, show default image
|
|
|
|
_cachedImageBytes = null;
|
|
|
|
return Image.asset(
|
|
|
|
_cachedImageDataHash = null;
|
|
|
|
defaultImage,
|
|
|
|
}
|
|
|
|
|
|
|
|
} 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,
|
|
|
|
width: 136.w,
|
|
|
|
height: 136.h,
|
|
|
|
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)
|
|
|
|
// Show default image (no image data or user has no uploaded image)
|
|
|
|
print('📷 Showing default avatar for user $currentPatientId');
|
|
|
|
print('📷 Showing default avatar for user $currentPatientId');
|
|
|
|
return Image.asset(
|
|
|
|
return Image.asset(
|
|
|
|
defaultImage,
|
|
|
|
defaultImage,
|
|
|
|
|
|
|
|
key: ValueKey('default_$currentPatientId'),
|
|
|
|
width: 136.w,
|
|
|
|
width: 136.w,
|
|
|
|
height: 136.h,
|
|
|
|
height: 136.h,
|
|
|
|
|
|
|
|
gaplessPlayback: true,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
// Check for user change on every build (handles navigation scenarios)
|
|
|
|
// Removed addPostFrameCallback from build() — it was causing redundant
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
// _checkAndUpdateUserImage calls on every single rebuild.
|
|
|
|
if (mounted) {
|
|
|
|
// didChangeDependencies + didUpdateWidget already handle user switches.
|
|
|
|
_checkAndUpdateUserImage();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Consumer<ProfileSettingsViewModel>(
|
|
|
|
return Consumer<ProfileSettingsViewModel>(
|
|
|
|
builder: (context, profileVm, _) {
|
|
|
|
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(
|
|
|
|
return Center(
|
|
|
|
child: Stack(
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
children: [
|
|
|
|
// Profile Image
|
|
|
|
// Profile Image — use AnimatedSwitcher to smooth transition
|
|
|
|
profileVm.isProfileImageLoading
|
|
|
|
AnimatedSwitcher(
|
|
|
|
? Container(
|
|
|
|
duration: const Duration(milliseconds: 200),
|
|
|
|
width: 136.w,
|
|
|
|
child: showShimmer
|
|
|
|
height: 136.h,
|
|
|
|
? Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
key: const ValueKey('shimmer'),
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
width: 136.w,
|
|
|
|
color: AppColors.greyTextColor.withValues(alpha: 0.2),
|
|
|
|
height: 136.h,
|
|
|
|
),
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
).toShimmer2(isShow: true)
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
: _buildProfileImage(profileVm),
|
|
|
|
color: AppColors.greyTextColor.withValues(alpha: 0.2),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
).toShimmer2(isShow: true)
|
|
|
|
|
|
|
|
: _buildProfileImage(profileVm),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Edit button
|
|
|
|
// Edit button
|
|
|
|
Positioned(
|
|
|
|
Positioned(
|
|
|
|
|