Merge branch 'master' into haroon_dev

pull/324/head
haroon amjad 3 weeks ago
commit f03939b9d0

@ -0,0 +1,343 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.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';
/// ViewModel for managing profile picture state and operations
/// Handles caching, loading, uploading, and user switching scenarios
class ProfilePictureViewModel extends ChangeNotifier {
final AppState _appState;
final ProfileSettingsViewModel _profileSettingsViewModel;
ProfilePictureViewModel({
required AppState appState,
required ProfileSettingsViewModel profileSettingsViewModel,
}) : _appState = appState,
_profileSettingsViewModel = profileSettingsViewModel;
// State variables
File? _selectedImage;
int? _currentPatientId;
bool _isInitialLoadTriggered = false;
/// Cache decoded image bytes to avoid decoding base64 on every rebuild
Uint8List? _cachedImageBytes;
String? _cachedImageDataHash;
/// ValueNotifier for targeted profile image updates (prevents full screen rebuild)
final ValueNotifier<int> _profileImageVersion = ValueNotifier<int>(0);
// Getters
File? get selectedImage => _selectedImage;
Uint8List? get cachedImageBytes => _cachedImageBytes;
bool get isInitialLoadTriggered => _isInitialLoadTriggered;
int? get currentPatientId => _currentPatientId;
ValueNotifier<int> get profileImageVersion => _profileImageVersion;
/// Initialize the provider with current user data
void initialize() {
_currentPatientId = _appState.getAuthenticatedUser()?.patientId;
_tryCacheExistingImage();
print('🔧 ProfilePictureViewModel initialized for patient: $_currentPatientId');
}
/// Trigger initial profile image load after first frame
void triggerInitialLoad() {
if (_isInitialLoadTriggered) return;
_isInitialLoadTriggered = true;
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) {
print('⚠️ No authenticated user found');
return;
}
print('📥 Loading fresh profile image from API for patient: $patientID');
loadProfileImage(forceRefresh: false);
}
/// 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}';
print('✅ Cached existing profile image');
} catch (e) {
print('❌ Error caching existing image: $e');
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
}
}
/// Check if authenticated user has changed (family member switch)
/// Returns true if user has changed
bool checkForUserSwitch() {
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
if (currentPatientId != null && currentPatientId != _currentPatientId) {
print('🔄 User switched detected: $_currentPatientId -> $currentPatientId');
_handleUserSwitch(currentPatientId);
return true;
}
return false;
}
/// Handle user switch scenario - clear caches and load new user's image
void _handleUserSwitch(int newPatientId) {
final oldPatientId = _currentPatientId;
_currentPatientId = newPatientId;
print('🧹 Clearing cache for old user: $oldPatientId');
// Clear AppState cache
_appState.clearProfileImageCache();
// Clear ViewModel cache
_profileSettingsViewModel.clearProfileImageCache();
// Clear local decoded bytes cache
_cachedImageBytes = null;
_cachedImageDataHash = null;
_selectedImage = null;
notifyListeners();
// Load new user's profile image
print('📥 Loading profile image for new user: $newPatientId');
_profileSettingsViewModel.getProfileImage(
patientID: newPatientId,
forceRefresh: true,
onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $newPatientId');
_tryCacheExistingImage();
notifyListeners();
},
onError: (error) {
print('❌ Error loading profile image: $error');
notifyListeners();
},
);
}
/// Load profile image from API
void loadProfileImage({bool forceRefresh = false}) {
// Check if profile image is already loaded in AppState (skip if forcing refresh)
if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
print('✅ Profile image already cached in AppState');
return;
}
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) {
print('⚠️ Cannot load profile image - no authenticated user');
return;
}
print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)');
_profileSettingsViewModel.getProfileImage(
patientID: patientID,
forceRefresh: forceRefresh,
onSuccess: (data) {
print('✅ Profile image loaded successfully');
_tryCacheExistingImage();
notifyListeners();
},
onError: (error) {
print('❌ Error loading profile image: $error');
},
);
}
/// Set selected image file (during upload process)
void setSelectedImage(File? file) {
_selectedImage = file;
notifyListeners();
}
/// Pick image from camera or gallery with compression
Future<void> pickImage(
BuildContext context, {
required void Function(
BuildContext context,
bool showFiles,
Function(String, File) onImageSelected, {
required Future<bool> Function() checkCameraPermission,
required Future<bool> Function() checkGalleryPermission,
}) showImagePicker,
required Future<File?> Function(File) compressImage,
required Function(String) onSuccess,
required Function(String) onError,
required String imageSizeTooLargeMessage,
required String failedToProcessImageMessage,
required Future<bool> Function(BuildContext) checkCameraPermission,
required Future<bool> Function(BuildContext) checkGalleryPermission,
}) async {
// Show image picker options
showImagePicker(
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 compressImage(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');
onError(imageSizeTooLargeMessage);
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');
onError(imageSizeTooLargeMessage);
return;
}
finalFile = compressedFile;
var bytes = await compressedFile.readAsBytes();
finalBase64 = base64.encode(bytes);
}
print('Converting to base64... Length: ${finalBase64.length}');
// Set selected image
setSelectedImage(finalFile);
print('📤 Starting upload...');
// Upload the image
uploadProfileImage(finalBase64, onSuccess: onSuccess, onError: onError);
print('=== Image processing complete ===');
} catch (e, stackTrace) {
print('❌ Error in pickImage: $e');
print('Stack trace: $stackTrace');
onError(failedToProcessImageMessage);
}
},
checkCameraPermission: () => checkCameraPermission(context),
checkGalleryPermission: () => checkGalleryPermission(context),
);
}
/// Upload profile image
void uploadProfileImage(
String base64String, {
required Function(String) onSuccess,
required Function(String) onError,
}) {
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) {
onError('No authenticated user found');
return;
}
print('📤 Uploading profile image for patient: $patientID');
_profileSettingsViewModel.uploadProfileImage(
patientID: patientID,
imageData: base64String,
onSuccess: (data) async {
print('✅ Profile image uploaded successfully');
// Clear old cache first to ensure fresh data
_cachedImageBytes = null;
_cachedImageDataHash = null;
// Add a small delay to ensure AppState is fully updated
await Future.delayed(const Duration(milliseconds: 50));
// Update cached bytes with the new data from AppState
_tryCacheExistingImage();
_selectedImage = null; // Clear selected image after successful upload
// Increment version to trigger targeted rebuild (no full screen refresh)
_profileImageVersion.value++;
print('🔄 Profile image version updated to ${_profileImageVersion.value} (targeted rebuild)');
onSuccess(data);
},
onError: (error) {
print('❌ Error uploading profile image: $error');
onError(error);
},
);
}
/// Update cached image bytes if source data has changed
void updateCacheIfNeeded() {
final String? imageData = _appState.getProfileImageData;
final String? currentHash = (imageData != null && imageData.isNotEmpty) ? '${imageData.length}_${imageData.hashCode}' : null;
// Re-decode only if the underlying data actually changed
if (currentHash != null && currentHash != _cachedImageDataHash) {
try {
_cachedImageBytes = base64Decode(imageData!);
_cachedImageDataHash = currentHash;
print('🔄 Updated cached image bytes');
} catch (e) {
print('❌ Error decoding profile image: $e');
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
} else if (currentHash == null) {
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
}
/// Clear all cached data
void clearCache() {
_cachedImageBytes = null;
_cachedImageDataHash = null;
_selectedImage = null;
notifyListeners();
print('🧹 Cleared all profile picture cache');
}
/// Check if we should show shimmer loading
bool shouldShowShimmer() {
return _profileSettingsViewModel.isProfileImageLoading && _cachedImageBytes == null && _selectedImage == null;
}
@override
void dispose() {
_profileImageVersion.dispose();
print('🗑️ ProfilePictureViewModel disposed');
super.dispose();
}
}
Loading…
Cancel
Save