Merge branch 'master' into haroon_dev
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…
Reference in New Issue