Merge branch 'master' into dev_aamir
# Conflicts: # lib/presentation/my_family/widget/family_cards.dartpull/225/head
commit
b4ab3a112c
@ -1,11 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30.996" height="28.414" viewBox="0 0 30.996 28.414">
|
||||
<g id="image-gallery" transform="translate(-0.001 -2)">
|
||||
<g id="Group_8635" data-name="Group 8635" transform="translate(0.001 5.875)">
|
||||
<g id="Group_8634" data-name="Group 8634" transform="translate(0 5.078)">
|
||||
<path id="Path_5018" data-name="Path 5018" d="M8.072,22.581A4.836,4.836,0,0,1,3.5,19.286l-.045-.149a4.711,4.711,0,0,1-.223-1.4V8.932L.1,19.391a2.933,2.933,0,0,0,2.056,3.558L22.123,28.3a2.962,2.962,0,0,0,.744.1,2.88,2.88,0,0,0,2.791-2.112l1.164-3.7Z" transform="translate(-0.001 -8.932)" fill="#2bb8a6"/>
|
||||
</g>
|
||||
<path id="Path_5019" data-name="Path 5019" d="M9.583,10.166A2.583,2.583,0,1,0,7,7.583,2.586,2.586,0,0,0,9.583,10.166Z" transform="translate(2.04 -5)" fill="#2bb8a6"/>
|
||||
</g>
|
||||
<path id="Path_5020" data-name="Path 5020" d="M26.6,2H7.229A3.233,3.233,0,0,0,4,5.229V19.436a3.233,3.233,0,0,0,3.229,3.229H26.6a3.233,3.233,0,0,0,3.229-3.229V5.229A3.233,3.233,0,0,0,26.6,2ZM7.229,4.583H26.6a.646.646,0,0,1,.646.646V14.4l-4.08-4.761a2.313,2.313,0,0,0-1.732-.794,2.258,2.258,0,0,0-1.726.815l-4.8,5.758-1.563-1.559a2.267,2.267,0,0,0-3.2,0L6.583,17.42V5.229a.646.646,0,0,1,.646-.646Z" transform="translate(1.166)" fill="#2bb8a6"/>
|
||||
</g>
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
|
||||
<path d="M21.2444 13.5C20.5571 13.5 20 14.0596 20 14.75C20 15.4404 20.5571 16 21.2444 16C21.9268 16 22.5 15.4401 22.5 14.75C22.5 14.0599 21.9268 13.5 21.2444 13.5Z" fill="#8F9AA3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.0732 9H17.9268C16.1034 8.99997 14.6336 8.99995 13.4777 9.15537C12.2775 9.31672 11.267 9.66191 10.4645 10.4645C9.66191 11.267 9.31672 12.2775 9.15537 13.4777C8.99995 14.6336 8.99997 16.1034 9 17.9268V18.0732C8.99997 19.8966 8.99995 21.3664 9.15537 22.5223C9.31672 23.7225 9.66191 24.733 10.4645 25.5355C11.1805 26.2516 12.062 26.6035 13.0961 26.7857C13.2742 27.8673 13.6274 28.7857 14.3689 29.5272C15.1752 30.3335 16.1906 30.6805 17.3977 30.8428C18.5606 30.9992 20.0394 30.9991 21.8748 30.9991H22.0212C23.8567 30.9991 25.3355 30.9992 26.4984 30.8428C27.7055 30.6805 28.7209 30.3335 29.5272 29.5272C30.3335 28.7209 30.6805 27.7055 30.8428 26.4984C30.9992 25.3355 30.9991 23.8567 30.9991 22.0212V21.8748C30.9991 20.0394 30.9992 18.5606 30.8428 17.3977C30.6805 16.1906 30.3335 15.1752 29.5272 14.3689C28.7857 13.6274 27.8673 13.2742 26.7857 13.0961C26.6035 12.062 26.2516 11.1805 25.5355 10.4645C24.733 9.66191 23.7225 9.31672 22.5223 9.15537C21.3664 8.99995 19.8966 8.99997 18.0732 9ZM26.9723 15.1769C27 15.9862 27 16.9005 27 17.9268V18.0732C27 19.8966 27.0001 21.3664 26.8446 22.5223C26.6833 23.7225 26.3381 24.733 25.5355 25.5355C24.733 26.3381 23.7225 26.6833 22.5223 26.8446C21.3664 27.0001 19.8966 27 18.0732 27H17.9268C16.9005 27 15.9862 27 15.1769 26.9723C15.3177 27.5048 15.5173 27.8471 15.7831 28.1129C16.1559 28.4857 16.6789 28.7282 17.6642 28.8606C18.6783 28.997 20.0221 28.9991 21.948 28.9991C23.874 28.9991 25.2178 28.997 26.2319 28.8606C27.2172 28.7282 27.7402 28.4857 28.1129 28.1129C28.4857 27.7402 28.7282 27.2172 28.8606 26.2319C28.997 25.2178 28.9991 23.874 28.9991 21.948C28.9991 20.0221 28.997 18.6783 28.8606 17.6642C28.7282 16.6789 28.4857 16.1559 28.1129 15.7831C27.8471 15.5173 27.5048 15.3177 26.9723 15.1769ZM11.8787 11.8787C12.2477 11.5097 12.7658 11.2691 13.7442 11.1375C14.7513 11.0021 16.0861 11 18 11C19.9139 11 21.2487 11.0021 22.2558 11.1375C23.2342 11.2691 23.7523 11.5097 24.1213 11.8787C24.4903 12.2477 24.7309 12.7658 24.8625 13.7442C24.9979 14.7513 25 16.0861 25 18C25 19.9139 24.9979 21.2487 24.8625 22.2558C24.7309 23.2342 24.4903 23.7523 24.1213 24.1213C23.9227 24.3199 23.6809 24.4813 23.3474 24.6086C22.5834 22.9592 21.3002 21.5131 19.6363 20.4162C17.4301 18.9618 14.6803 18.2023 11.8763 18.2523C11.5839 18.2518 11.2916 18.2596 11 18.2755L11 18C11 16.0861 11.0021 14.7513 11.1375 13.7442C11.2691 12.7658 11.5097 12.2477 11.8787 11.8787Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.7 KiB |
@ -1,7 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="31.908" height="29.134" viewBox="0 0 31.908 29.134">
|
||||
<g id="Group_8579" data-name="Group 8579" transform="translate(0 -100)">
|
||||
<path id="Path_4556" data-name="Path 4556" d="M88.972,88.108h-4.4l-2.539-2.775H72.324v4.162H68.162v4.162H64V107.53a2.783,2.783,0,0,0,2.775,2.775h22.2a2.783,2.783,0,0,0,2.775-2.775V90.882A2.783,2.783,0,0,0,88.972,88.108Zm-11.1,18.035a6.937,6.937,0,1,1,6.937-6.937A6.939,6.939,0,0,1,77.873,106.143Z" transform="translate(-59.838 18.829)" fill="#2bb8a6"/>
|
||||
<path id="Path_4557" data-name="Path 4557" d="M4.162,32.432H6.937V28.27H11.1V25.495H6.937V21.333H4.162v4.162H0V28.27H4.162Z" transform="translate(0 78.667)" fill="#2bb8a6"/>
|
||||
<path id="Path_4558" data-name="Path 4558" d="M213.505,230.4a4.435,4.435,0,0,0-4.439,4.439h0a4.439,4.439,0,1,0,4.439-4.439Z" transform="translate(-195.47 -116.804)" fill="#2bb8a6"/>
|
||||
</g>
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9686 9.25H20.0314H20.0314C20.6355 9.24999 21.1356 9.24998 21.5485 9.28515C21.9812 9.32201 22.3723 9.40069 22.7494 9.58742C23.4214 9.92009 23.8461 10.4488 24.1443 10.979C24.3755 11.39 24.555 11.8518 24.7071 12.2432C24.743 12.3355 24.7774 12.424 24.8105 12.5069L24.9569 12.8728C25.0299 13.0554 25.0664 13.1466 25.1425 13.1982C25.2185 13.2498 25.317 13.2499 25.5141 13.2502C26.3986 13.2516 27.0637 13.2617 27.5921 13.3345C28.2753 13.4287 28.7694 13.6309 29.2829 14.0249C29.5429 14.2244 29.7756 14.4572 29.9751 14.7171C30.4019 15.2734 30.5828 15.922 30.6678 16.6762C30.75 17.4057 30.75 18.3204 30.75 19.4548V24.0549C30.75 25.4225 30.75 26.5248 30.6335 27.3918C30.5125 28.2919 30.2536 29.0497 29.6517 29.6517C29.0497 30.2536 28.2919 30.5125 27.3918 30.6335C26.5248 30.75 25.4225 30.75 24.0549 30.75L15.9451 30.75C14.5775 30.75 13.4752 30.75 12.6083 30.6335C11.7081 30.5125 10.9503 30.2536 10.3484 29.6517C9.74644 29.0497 9.48754 28.2919 9.36653 27.3918C9.24997 26.5248 9.24998 25.4225 9.25 24.0549V24.0549L9.25 19.4548V19.4548C9.24999 18.3204 9.24998 17.4057 9.33222 16.6762C9.41725 15.922 9.59808 15.2734 10.0249 14.7171C10.2244 14.4572 10.4572 14.2244 10.7171 14.0249C11.2306 13.6309 11.7247 13.4287 12.4079 13.3345C12.9363 13.2617 13.6014 13.2516 14.4859 13.2502C14.683 13.2499 14.7815 13.2498 14.8575 13.1982C14.9336 13.1466 14.9701 13.0554 15.0431 12.8728L15.1895 12.5069C15.2226 12.424 15.257 12.3355 15.2929 12.2432C15.445 11.8518 15.6245 11.39 15.8557 10.979C16.1539 10.4488 16.5787 9.92009 17.2506 9.58742C17.6278 9.40069 18.0188 9.32201 18.4515 9.28515C18.8644 9.24998 19.3645 9.24999 19.9686 9.25H19.9686ZM16 22C16 19.7909 17.7909 18 20 18C22.2091 18 24 19.7909 24 22C24 24.2091 22.2091 26 20 26C17.7909 26 16 24.2091 16 22ZM19.9955 13C19.4457 13 19 13.4477 19 14C19 14.5502 19.4567 15 20.0045 15C20.5543 15 21 14.5523 21 14C21 13.4498 20.5433 13 19.9955 13Z" fill="#8F9AA3"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 910 B After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,103 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class ImageCompressionHelper {
|
||||
/// Compress and resize image to max 200x200 and ensure file size < 1MB
|
||||
/// Returns the compressed image file
|
||||
static Future<File?> compressAndResizeImage(File imageFile) async {
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!await imageFile.exists()) {
|
||||
print('Image file does not exist');
|
||||
return null;
|
||||
}
|
||||
|
||||
print('Original file size: ${await imageFile.length() / 1024} KB');
|
||||
|
||||
// Get temporary directory
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final targetPath = path.join(
|
||||
tempDir.path,
|
||||
'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
);
|
||||
|
||||
// Start with quality 85
|
||||
int quality = 85;
|
||||
const int maxFileSizeBytes = 1048576; // 1MB
|
||||
|
||||
// Compress the image
|
||||
var result = await FlutterImageCompress.compressWithFile(
|
||||
imageFile.absolute.path,
|
||||
quality: quality,
|
||||
format: CompressFormat.jpeg,
|
||||
);
|
||||
|
||||
if (result == null || result.isEmpty) {
|
||||
print('Compression returned null or empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Write to file
|
||||
File resultFile = File(targetPath);
|
||||
await resultFile.writeAsBytes(result);
|
||||
|
||||
int fileSize = await resultFile.length();
|
||||
print('First compression: ${fileSize / 1024} KB, quality: $quality');
|
||||
|
||||
// If file size is still > 1MB, reduce quality progressively
|
||||
int attempts = 0;
|
||||
while (fileSize > maxFileSizeBytes && quality > 10 && attempts < 10) {
|
||||
quality -= 10;
|
||||
attempts++;
|
||||
|
||||
result = await FlutterImageCompress.compressWithFile(
|
||||
imageFile.absolute.path,
|
||||
quality: quality,
|
||||
format: CompressFormat.jpeg,
|
||||
);
|
||||
|
||||
if (result == null || result.isEmpty) {
|
||||
print('Compression returned null on attempt $attempts');
|
||||
break;
|
||||
}
|
||||
|
||||
await resultFile.writeAsBytes(result);
|
||||
fileSize = await resultFile.length();
|
||||
print('Compression attempt $attempts: ${fileSize / 1024} KB, quality: $quality');
|
||||
}
|
||||
|
||||
// Final validation
|
||||
if (await resultFile.exists()) {
|
||||
final finalSize = await resultFile.length();
|
||||
print('Final compressed image size: ${finalSize / 1024} KB');
|
||||
|
||||
if (finalSize > maxFileSizeBytes) {
|
||||
print('Warning: Final file size (${finalSize / 1024} KB) exceeds 1MB limit');
|
||||
}
|
||||
|
||||
return resultFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e, stackTrace) {
|
||||
print('Error compressing image: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get file size in KB
|
||||
static Future<double> getFileSizeInKB(File file) async {
|
||||
int bytes = await file.length();
|
||||
return bytes / 1024;
|
||||
}
|
||||
|
||||
/// Get file size in MB
|
||||
static Future<double> getFileSizeInMB(File file) async {
|
||||
int bytes = await file.length();
|
||||
return bytes / (1024 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,511 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/image_compression_helper.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/image_picker.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProfilePictureWidget extends StatefulWidget {
|
||||
const ProfilePictureWidget({super.key});
|
||||
|
||||
@override
|
||||
State<ProfilePictureWidget> createState() => _ProfilePictureWidgetState();
|
||||
}
|
||||
|
||||
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
|
||||
final AppState _appState = getIt.get<AppState>();
|
||||
File? _selectedImage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Use addPostFrameCallback to ensure widget is built before loading
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
|
||||
final profileVm = context.read<ProfileSettingsViewModel>();
|
||||
|
||||
// Only sync if AppState has data and ViewModel doesn't
|
||||
if (_appState.getProfileImageData != null &&
|
||||
_appState.getProfileImageData!.isNotEmpty &&
|
||||
(profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) {
|
||||
profileVm.syncProfileImageFromAppState();
|
||||
}
|
||||
|
||||
// Only load if no data exists in both AppState and ViewModel
|
||||
if ((_appState.getProfileImageData == null || _appState.getProfileImageData!.isEmpty) &&
|
||||
(profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) {
|
||||
_loadProfileImage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _loadProfileImage() {
|
||||
// Check if profile image is already loaded in AppState
|
||||
if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
|
||||
// Image already loaded, no need to call API
|
||||
return;
|
||||
}
|
||||
|
||||
final profileVm = context.read<ProfileSettingsViewModel>();
|
||||
final patientID = _appState.getAuthenticatedUser()?.patientId;
|
||||
|
||||
if (patientID != null) {
|
||||
profileVm.getProfileImage(
|
||||
patientID: patientID,
|
||||
onSuccess: (data) {
|
||||
// Image loaded successfully
|
||||
},
|
||||
onError: (error) {
|
||||
// Error loading image
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _pickImage() {
|
||||
// Show image picker options without checking permissions first
|
||||
ImageOptions.showImageOptionsNew(
|
||||
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 ImageCompressionHelper.compressAndResizeImage(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');
|
||||
if (mounted) {
|
||||
Utils.showToast(
|
||||
LocaleKeys.imageSizeTooLarge.tr(context: context),
|
||||
);
|
||||
}
|
||||
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');
|
||||
if (mounted) {
|
||||
Utils.showToast(
|
||||
LocaleKeys.imageSizeTooLarge.tr(context: context),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
finalFile = compressedFile;
|
||||
var bytes = await compressedFile.readAsBytes();
|
||||
finalBase64 = base64.encode(bytes);
|
||||
}
|
||||
|
||||
print('Converting to base64... Length: ${finalBase64.length}');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedImage = finalFile;
|
||||
});
|
||||
|
||||
print('📤 Starting upload...');
|
||||
// Upload the image
|
||||
_uploadImage(finalBase64);
|
||||
}
|
||||
|
||||
print('=== Image processing complete ===');
|
||||
} catch (e, stackTrace) {
|
||||
print('❌ Error in _pickImage: $e');
|
||||
print('Stack trace: $stackTrace');
|
||||
if (mounted) {
|
||||
Utils.showToast(
|
||||
LocaleKeys.failedToProcessImage.tr(context: context),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
checkCameraPermission: _checkCameraPermission,
|
||||
checkGalleryPermission: _checkGalleryPermission,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _checkCameraPermission() async {
|
||||
try {
|
||||
print('=== Checking camera permission ===');
|
||||
|
||||
// First check current status
|
||||
PermissionStatus currentStatus = await Permission.camera.status;
|
||||
print('Current camera permission status: $currentStatus');
|
||||
|
||||
// If already granted, return true
|
||||
if (currentStatus.isGranted) {
|
||||
print('✅ Camera permission already granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// If denied or permanently denied, show settings dialog
|
||||
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
|
||||
// Request permission first
|
||||
PermissionStatus newStatus = await Permission.camera.request();
|
||||
print('Camera permission after request: $newStatus');
|
||||
|
||||
if (newStatus.isGranted) {
|
||||
print('✅ Camera permission granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Still denied - show settings dialog
|
||||
print('⚠️ Camera permission denied - showing settings dialog');
|
||||
if (mounted) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onConfirmTap: () async {
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request permission for the first time
|
||||
PermissionStatus newStatus = await Permission.camera.request();
|
||||
print('Camera permission after request: $newStatus');
|
||||
|
||||
if (newStatus.isGranted) {
|
||||
print('✅ Camera permission granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Denied - show settings dialog
|
||||
print('❌ Camera permission denied - showing settings dialog');
|
||||
if (mounted) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context),
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onConfirmTap: () async {
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
print('❌ Error checking camera permission: $e');
|
||||
if (mounted) {
|
||||
Utils.showToast(
|
||||
LocaleKeys.failedToCheckPermissions.tr(context: context),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkGalleryPermission() async {
|
||||
try {
|
||||
print('=== Checking gallery permission ===');
|
||||
|
||||
// Determine which permission to check based on platform and Android version
|
||||
Permission galleryPermission;
|
||||
|
||||
if (Platform.isIOS) {
|
||||
galleryPermission = Permission.photos;
|
||||
} else {
|
||||
// Android: use photos permission which handles API level differences automatically
|
||||
galleryPermission = Permission.photos;
|
||||
}
|
||||
|
||||
// First check current status
|
||||
PermissionStatus currentStatus = await galleryPermission.status;
|
||||
print('Current gallery permission status: $currentStatus');
|
||||
|
||||
// If already granted, return true
|
||||
if (currentStatus.isGranted || currentStatus.isLimited) {
|
||||
print('✅ Gallery permission already granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// If denied or permanently denied, request permission first
|
||||
if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) {
|
||||
// Request permission first
|
||||
PermissionStatus newStatus = await galleryPermission.request();
|
||||
print('Gallery permission after request: $newStatus');
|
||||
|
||||
if (newStatus.isGranted || newStatus.isLimited) {
|
||||
print('✅ Gallery permission granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Still denied - show settings dialog
|
||||
print('⚠️ Gallery permission denied - showing settings dialog');
|
||||
if (mounted) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onConfirmTap: () async {
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request permission for the first time
|
||||
PermissionStatus newStatus = await galleryPermission.request();
|
||||
print('Gallery permission after request: $newStatus');
|
||||
|
||||
if (newStatus.isGranted || newStatus.isLimited) {
|
||||
print('✅ Gallery permission granted');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Denied - show settings dialog
|
||||
print('❌ Gallery permission denied - showing settings dialog');
|
||||
if (mounted) {
|
||||
showCommonBottomSheetWithoutHeight(
|
||||
title: LocaleKeys.notice.tr(context: context),
|
||||
context,
|
||||
child: Utils.getWarningWidget(
|
||||
loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context),
|
||||
isShowActionButtons: true,
|
||||
onCancelTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onConfirmTap: () async {
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
callBackFunc: () {},
|
||||
isFullScreen: false,
|
||||
isCloseButtonVisible: true,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
print('❌ Error checking gallery permission: $e');
|
||||
if (mounted) {
|
||||
Utils.showToast(
|
||||
LocaleKeys.failedToCheckPermissions.tr(context: context),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _uploadImage(String base64String) {
|
||||
final profileVm = context.read<ProfileSettingsViewModel>();
|
||||
final patientID = _appState.getAuthenticatedUser()?.patientId;
|
||||
|
||||
if (patientID != null) {
|
||||
profileVm.uploadProfileImage(
|
||||
patientID: patientID,
|
||||
imageData: base64String,
|
||||
onSuccess: (data) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedImage = null; // Clear selected image after successful upload
|
||||
});
|
||||
print(
|
||||
LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
Utils.showToast(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProfileImage(ProfileSettingsViewModel profileVm) {
|
||||
final gender = _appState.getAuthenticatedUser()?.gender ?? 1;
|
||||
final age = _appState.getAuthenticatedUser()?.age ?? 0;
|
||||
|
||||
// Determine the default image based on gender and age
|
||||
final String defaultImage;
|
||||
if (gender == 1) {
|
||||
// Male
|
||||
defaultImage = age < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg;
|
||||
} else {
|
||||
// Female
|
||||
defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg;
|
||||
}
|
||||
|
||||
// Show selected image if available
|
||||
if (_selectedImage != null) {
|
||||
return ClipOval(
|
||||
child: Image.file(
|
||||
_selectedImage!,
|
||||
width: 136.w,
|
||||
height: 136.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Check both ViewModel and AppState for profile image
|
||||
final String? imageData = profileVm.profileImageData ?? _appState.getProfileImageData;
|
||||
|
||||
// Show uploaded image if available
|
||||
if (imageData != null && imageData.isNotEmpty) {
|
||||
try {
|
||||
final bytes = base64Decode(imageData);
|
||||
return ClipOval(
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
width: 136.w,
|
||||
height: 136.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error decoding profile image: $e');
|
||||
// If decoding fails, show default image
|
||||
return Image.asset(
|
||||
defaultImage,
|
||||
width: 136.w,
|
||||
height: 136.h,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Show default image
|
||||
return Image.asset(
|
||||
defaultImage,
|
||||
width: 136.w,
|
||||
height: 136.h,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfileSettingsViewModel>(
|
||||
builder: (context, profileVm, _) {
|
||||
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),
|
||||
|
||||
// Edit button
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!profileVm.isUploadingProfileImage) {
|
||||
_pickImage();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 32.w,
|
||||
height: 32.h,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryRedColor,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.whiteColor,
|
||||
width: 2.w,
|
||||
),
|
||||
),
|
||||
child: profileVm.isUploadingProfileImage
|
||||
? SizedBox(
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.w,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.whiteColor,
|
||||
),
|
||||
).paddingAll(8.w),
|
||||
)
|
||||
: Icon(
|
||||
Icons.camera_alt,
|
||||
color: AppColors.whiteColor,
|
||||
size: 16.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(bottom: 24.h),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.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/utils/size_utils.dart';
|
||||
|
||||
class UserAvatarWidget extends StatelessWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final int? gender;
|
||||
final int? age;
|
||||
final BoxFit? fit;
|
||||
final bool isCircular;
|
||||
final double? borderRadius;
|
||||
final String? customProfileImageData; // For family members or other users
|
||||
|
||||
const UserAvatarWidget({
|
||||
Key? key,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.gender,
|
||||
this.age,
|
||||
this.fit,
|
||||
this.isCircular = false,
|
||||
this.borderRadius,
|
||||
this.customProfileImageData,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = getIt.get<AppState>();
|
||||
final userGender = gender ?? appState.getAuthenticatedUser()?.gender ?? 1;
|
||||
final userAge = age ?? appState.getAuthenticatedUser()?.age ?? 0;
|
||||
|
||||
// Use custom profile image data if provided, otherwise use AppState data
|
||||
final profileImageData = customProfileImageData ?? appState.getProfileImageData;
|
||||
|
||||
|
||||
// Determine the default image based on gender and age
|
||||
final String defaultImage;
|
||||
if (userGender == 1) {
|
||||
// Male
|
||||
defaultImage = userAge < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg;
|
||||
} else {
|
||||
// Female
|
||||
defaultImage = userAge < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg;
|
||||
}
|
||||
|
||||
// Calculate border radius - default to 12.r if not specified and not circular
|
||||
final effectiveBorderRadius = isCircular ? width / 2 : (borderRadius ?? 12.r);
|
||||
|
||||
// Show uploaded profile image if available
|
||||
if (profileImageData != null && profileImageData.isNotEmpty) {
|
||||
try {
|
||||
final bytes = base64Decode(profileImageData);
|
||||
final imageWidget = Image.memory(
|
||||
bytes,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit ?? BoxFit.cover,
|
||||
);
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(effectiveBorderRadius),
|
||||
child: imageWidget,
|
||||
);
|
||||
} catch (e) {
|
||||
// If decoding fails, show default image
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(effectiveBorderRadius),
|
||||
child: Image.asset(
|
||||
defaultImage,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Show default image with rounded corners
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(effectiveBorderRadius),
|
||||
child: Image.asset(
|
||||
defaultImage,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue