diff --git a/lib/features/profile_picture/profile_picture_view_model.dart b/lib/features/profile_picture/profile_picture_view_model.dart new file mode 100644 index 00000000..017ca5d8 --- /dev/null +++ b/lib/features/profile_picture/profile_picture_view_model.dart @@ -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 _profileImageVersion = ValueNotifier(0); + + // Getters + File? get selectedImage => _selectedImage; + + Uint8List? get cachedImageBytes => _cachedImageBytes; + + bool get isInitialLoadTriggered => _isInitialLoadTriggered; + + int? get currentPatientId => _currentPatientId; + + ValueNotifier 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 pickImage( + BuildContext context, { + required void Function( + BuildContext context, + bool showFiles, + Function(String, File) onImageSelected, { + required Future Function() checkCameraPermission, + required Future Function() checkGalleryPermission, + }) showImagePicker, + required Future Function(File) compressImage, + required Function(String) onSuccess, + required Function(String) onError, + required String imageSizeTooLargeMessage, + required String failedToProcessImageMessage, + required Future Function(BuildContext) checkCameraPermission, + required Future 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(); + } +}