import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:taxglide/consts/comman_image_box.dart'; import 'package:taxglide/consts/comman_textformfileds.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:path/path.dart' as path; /// A reusable helper class for image picking with safe permission handling. class ImagePickerHelper { static final ImagePicker _picker = ImagePicker(); static bool _isPicking = false; /// Request permission depending on platform and SDK version static Future _requestPermission( BuildContext context, ImageSource source, ) async { try { Permission permission; String name; if (source == ImageSource.camera) { permission = Permission.camera; name = 'Camera'; } else { if (Platform.isAndroid) { final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt >= 33) { permission = Permission.photos; name = 'Photos'; } else { permission = Permission.storage; name = 'Storage'; } } else { permission = Permission.photos; // iOS gallery name = 'Gallery'; } } var status = await permission.status; if (status.isGranted || status.isLimited) return true; // On iOS, if already denied, request() won't show anything. // On Android, permanently denied also won't show anything. if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) { await _showSettingsDialog(context, name); return false; } // Now request permission status = await permission.request(); if (status.isGranted || status.isLimited) { return true; } else { // If user denied it just now, or it was already denied (iOS) await _showSettingsDialog(context, name); return false; } } catch (e) { debugPrint('Permission request failed: $e'); return Platform.isIOS; } } static Future _showSettingsDialog( BuildContext context, String name, ) async { final openSettings = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('$name Permission Required'), content: Text( 'Allow Taxglide to access your $name to capture and upload files easily. You can enable this in the app settings.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Go to Settings'), ), ], ), ) ?? false; if (openSettings) await openAppSettings(); } /// Pick single image safely static Future pickSingleImage({ required BuildContext context, required void Function(File?) onImageSelected, required void Function(String?) setError, }) async { if (_isPicking) return; _isPicking = true; try { // Ask user: Camera or Gallery final source = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: const Text('Select Image Source'), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.camera_alt, color: Colors.blue), title: const Text('Camera'), onTap: () => Navigator.pop(ctx, ImageSource.camera), ), ListTile( leading: const Icon(Icons.photo, color: Colors.blue), title: const Text('Gallery'), onTap: () => Navigator.pop(ctx, ImageSource.gallery), ), ], ), ), ); if (source == null) { _isPicking = false; return; } // Permission check final hasPermission = await _requestPermission(context, source); if (!hasPermission) { _isPicking = false; return; } // Image selection final pickedFile = await _picker.pickImage(source: source); if (pickedFile != null) { onImageSelected(File(pickedFile.path)); setError(null); } } catch (e) { debugPrint('Image pick error: $e'); Get.snackbar( "Error", "Failed to pick image: $e", backgroundColor: Colors.red, colorText: Colors.white, ); } finally { _isPicking = false; } } /// 🔥 NEW: Pick Image or PDF static Future pickImageOrPdf({ required BuildContext context, required void Function(File?) onFileSelected, required void Function(String?) setError, }) async { if (_isPicking) return; _isPicking = true; try { // Ask user: Camera, Gallery, or PDF final choice = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: const Text('Select File Source'), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.camera_alt, color: Colors.blue), title: const Text('Camera'), onTap: () => Navigator.pop(ctx, 'camera'), ), ListTile( leading: const Icon(Icons.photo, color: Colors.blue), title: const Text('Gallery (Image)'), onTap: () => Navigator.pop(ctx, 'gallery'), ), ListTile( leading: const Icon(Icons.picture_as_pdf, color: Colors.red), title: const Text('Select PDF'), onTap: () => Navigator.pop(ctx, 'pdf'), ), ], ), ), ); if (choice == null) { _isPicking = false; return; } if (choice == 'pdf') { // Pick PDF using file_picker final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf'], ); if (result != null && result.files.single.path != null) { final file = File(result.files.single.path!); // Check file size (max 10MB) final fileSizeInBytes = await file.length(); final fileSizeInMB = fileSizeInBytes / (1024 * 1024); if (fileSizeInMB > 10) { Get.snackbar( "Error", "PDF file size should not exceed 10MB", backgroundColor: Colors.red, colorText: Colors.white, ); _isPicking = false; return; } onFileSelected(file); setError(null); } } else { // Pick Image (Camera or Gallery) final source = choice == 'camera' ? ImageSource.camera : ImageSource.gallery; // Permission check final hasPermission = await _requestPermission(context, source); if (!hasPermission) { _isPicking = false; return; } // Image selection final pickedFile = await _picker.pickImage(source: source); if (pickedFile != null) { onFileSelected(File(pickedFile.path)); setError(null); } } } catch (e) { debugPrint('File pick error: $e'); Get.snackbar( "Error", "Failed to pick file: $e", backgroundColor: Colors.red, colorText: Colors.white, ); } finally { _isPicking = false; } } } class LabeledTextField extends StatelessWidget { final GlobalKey fieldKey; final String label; final String keyName; final String hint; final TextEditingController controller; final FocusNode focusNode; final TextInputType keyboardType; final Map errors; final Function(String) onChanged; final bool readOnly; /// ✅ ADD THESE final List? inputFormatters; final TextCapitalization textCapitalization; const LabeledTextField({ super.key, required this.fieldKey, required this.label, required this.keyName, required this.hint, required this.controller, required this.focusNode, required this.errors, required this.onChanged, this.readOnly = false, this.keyboardType = TextInputType.text, /// ✅ DEFAULT VALUES this.inputFormatters, this.textCapitalization = TextCapitalization.none, }); @override Widget build(BuildContext context) { return Padding( key: fieldKey, padding: const EdgeInsets.only(bottom: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontFamily: "Gilroy", fontWeight: FontWeight.w600, fontSize: 16, height: 1.3, letterSpacing: 0.64, color: Colors.black, ), ), const SizedBox(height: 8), /// ✅ PASS FORMATTERS & CAPITALIZATION HERE CommanTextFormField( controller: controller, hintText: hint, keyboardType: keyboardType, focusNode: focusNode, hasError: errors[keyName] != null, onChanged: onChanged, readOnly: readOnly, inputFormatters: inputFormatters, textCapitalization: textCapitalization, ), if (errors[keyName] != null) Padding( padding: const EdgeInsets.only(top: 4, left: 6), child: Text( errors[keyName]!, style: const TextStyle(color: Colors.red, fontSize: 12), ), ), ], ), ); } } // 🔥 Updated SingleImageSectionField with URL support (Image only - for Logo) class SingleImageSectionField extends StatelessWidget { final GlobalKey imageKey; final String title; final File? imageFile; final String? imageUrl; final String? errorText; final Function(String?) setError; final Function(File?) onImageSelected; final VoidCallback onImageRemoved; const SingleImageSectionField({ super.key, required this.imageKey, required this.title, required this.imageFile, required this.imageUrl, required this.errorText, required this.setError, required this.onImageSelected, required this.onImageRemoved, }); @override Widget build(BuildContext context) { final hasImage = imageFile != null || imageUrl != null; return Padding( key: imageKey, padding: const EdgeInsets.only(bottom: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: "Gilroy", fontWeight: FontWeight.w600, fontSize: 16, height: 1.3, letterSpacing: 0.64, color: Colors.black, ), ), const SizedBox(height: 8), CommanImageBox( onTap: () => ImagePickerHelper.pickSingleImage( context: context, onImageSelected: onImageSelected, setError: setError, ), text: hasImage ? 'Change Image' : 'Upload Image', ), if (errorText != null) Padding( padding: const EdgeInsets.only(top: 4, left: 6), child: Text( errorText!, style: const TextStyle(color: Colors.red, fontSize: 12), ), ), if (hasImage) Container( margin: const EdgeInsets.only(top: 10), child: Stack( children: [ Container( width: 120, height: 120, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: imageFile != null ? Image.file(imageFile!, fit: BoxFit.cover) : CachedNetworkImage( imageUrl: imageUrl!, fit: BoxFit.cover, placeholder: (context, url) => const Center( child: CircularProgressIndicator(), ), errorWidget: (context, url, error) => const Icon(Icons.error, color: Colors.red), ), ), ), Positioned( right: 0, top: 0, child: GestureDetector( onTap: onImageRemoved, child: Container( decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 4, ), ], ), padding: const EdgeInsets.all(4), child: const Icon( Icons.close, color: Colors.white, size: 18, ), ), ), ), ], ), ), ], ), ); } } // 🔥 NEW: File Section Field (Supports both Image and PDF) class FileUploadSectionField extends StatelessWidget { final GlobalKey fileKey; final String title; final File? fileData; final String? fileUrl; final String? errorText; final Function(String?) setError; final Function(File?) onFileSelected; final VoidCallback onFileRemoved; const FileUploadSectionField({ super.key, required this.fileKey, required this.title, required this.fileData, required this.fileUrl, required this.errorText, required this.setError, required this.onFileSelected, required this.onFileRemoved, }); bool _isPdfFile(String? filePath) { if (filePath == null) return false; return path.extension(filePath).toLowerCase() == '.pdf'; } bool _isImageFile(String? filePath) { if (filePath == null) return false; final ext = path.extension(filePath).toLowerCase(); return ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].contains(ext); } @override Widget build(BuildContext context) { final hasFile = fileData != null || fileUrl != null; final isPdf = _isPdfFile(fileData?.path ?? fileUrl); final isImage = _isImageFile(fileData?.path ?? fileUrl); return Padding( key: fileKey, padding: const EdgeInsets.only(bottom: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: "Gilroy", fontWeight: FontWeight.w600, fontSize: 16, height: 1.3, letterSpacing: 0.64, color: Colors.black, ), ), const SizedBox(height: 8), CommanImageBox( onTap: () => ImagePickerHelper.pickImageOrPdf( context: context, onFileSelected: onFileSelected, setError: setError, ), text: hasFile ? 'Change File' : 'Upload Image/PDF', ), if (errorText != null) Padding( padding: const EdgeInsets.only(top: 4, left: 6), child: Text( errorText!, style: const TextStyle(color: Colors.red, fontSize: 12), ), ), // Display file preview if (hasFile) Container( margin: const EdgeInsets.only(top: 10), child: Stack( children: [ Container( width: 120, height: 120, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: isPdf // Show PDF icon ? Container( color: Colors.red.shade50, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.picture_as_pdf, size: 48, color: Colors.red.shade700, ), const SizedBox(height: 8), Text( 'PDF', style: TextStyle( color: Colors.red.shade700, fontWeight: FontWeight.bold, ), ), ], ), ) : isImage && fileData != null // Show local image ? Image.file(fileData!, fit: BoxFit.cover) : isImage && fileUrl != null // Show network image ? CachedNetworkImage( imageUrl: fileUrl!, fit: BoxFit.cover, placeholder: (context, url) => const Center( child: CircularProgressIndicator(), ), errorWidget: (context, url, error) => const Icon(Icons.error, color: Colors.red), ) // Unknown file type : Container( color: Colors.grey.shade100, child: const Icon( Icons.insert_drive_file, size: 48, color: Colors.grey, ), ), ), ), Positioned( right: 0, top: 0, child: GestureDetector( onTap: onFileRemoved, child: Container( decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 4, ), ], ), padding: const EdgeInsets.all(4), child: const Icon( Icons.close, color: Colors.white, size: 18, ), ), ), ), ], ), ), ], ), ); } }