import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:image_blur_detection/image_blur_detection.dart'; import 'package:image_picker/image_picker.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/models/generic_attachment_model.dart'; import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import '../../../new_views/common_widgets/app_dashed_button.dart'; import 'multi_image_picker_item.dart'; class AttachmentModel { int id = 0; File? file; AttachmentModel(this.id, this.file); factory AttachmentModel.fromJson(Map json) { return AttachmentModel( json['id'] ?? 0, json['file'] != null ? File(json['file']) : null, ); } Map toJson() { return { 'id': id, 'file': file?.path, }; } } class AttachmentPicker extends StatefulWidget { final String pickerTitle; final String label; final bool error; final List attachment; final bool enabled, onlyImages; double? buttonHeight; Widget? buttonIcon; Color? buttonColor; final Function(List)? onChange; final bool showAsListView; final bool pickMultiple; Lookup? documentType; Widget? child; bool checkQuality; AttachmentPicker( {Key? key, this.attachment = const [], required this.label, this.pickerTitle = "Attach File", this.error = false, this.buttonHeight, this.buttonIcon, this.enabled = true, this.onlyImages = false, this.onChange, this.documentType, this.showAsListView = false, this.pickMultiple = false, this.checkQuality = false, this.child, this.buttonColor}) : super(key: key); @override State createState() => _AttachmentPickerState(); } class _AttachmentPickerState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 1200), vsync: this, ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return widget.child != null ? widget.child!.onPress((widget.enabled == false) ? () {} : () => onFilePicker(widget.checkQuality)) : Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 12, children: [ AppDashedButton( title: widget.label, height: widget.buttonHeight, buttonColor: widget.buttonColor, icon: widget.buttonIcon, onPressed: (widget.enabled == false) ? () {} : () => onFilePicker(widget.checkQuality)), if (widget.attachment.isNotEmpty) widget.showAsListView ? ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 0), itemBuilder: (cxt, index) { File image = File(widget.attachment[index].name!); return MultiFilesPickerItem( file: image, showAsListView: widget.showAsListView, enabled: widget.enabled, documentType: widget.attachment[index].documentTypeId?.name, onRemoveTap: (image) { if (!widget.enabled) { return; } widget.attachment.removeAt(index); if (widget.onChange != null) { widget.onChange!(widget.attachment); } setState(() {}); }, ); }, separatorBuilder: (cxt, index) => 8.height, itemCount: widget.attachment.length) : Wrap( spacing: 8.toScreenWidth, children: List.generate( widget.attachment.length, (index) { File image = File(widget.attachment[index].name!); return MultiFilesPickerItem( file: image, enabled: widget.enabled, onRemoveTap: (image) { if (!widget.enabled) { return; } widget.attachment.removeAt(index); if (widget.onChange != null) { widget.onChange!(widget.attachment); } setState(() {}); }, ); }, ), ).paddingOnly(top: 0), ], ); } fromFilePicker() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowMultiple: true, allowedExtensions: widget.onlyImages ? ['jpg', 'jpeg', 'png'] : ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xlsx', 'pptx'], ); if (result != null) { for (var path in result.paths) { widget.attachment.add(GenericAttachmentModel(id: 0, name: File(path!).path, documentTypeId: widget.documentType)); } if (widget.onChange != null) { widget.onChange!(widget.attachment); } setState(() {}); } } Widget gridItem(IconData iconData, String title) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColor.background(context), borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColor.borderColor(context), width: 2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon(iconData, color: AppColor.icon2Color(context), size: 24), title.bodyText2(context).custom(color: AppColor.labelTextStyleColor(context), fontSize: context.isTablet() ? 10.toScreenWidth : null), // Text( // title, // style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), // ), ], ), ); } onFilePicker(bool checkQuality) async { // Reset and restart animation for bottom sheet _animationController.reset(); _animationController.forward(); ImageSource? source = await showModalBottomSheet( context: context, builder: (BuildContext context) { Widget listCard({required String icon, required String label, required VoidCallback onTap}) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColor.background(context), borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColor.borderColor(context), width: 2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ icon.toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)), label.bodyText2(context).custom(color: AppColor.labelTextStyleColor(context), fontSize: context.isTablet() ? 10.toScreenWidth : null), ], ), ).onPress(onTap); } List listItems = [ listCard( icon: 'camera_icon', label: '${context.translation.open}\n${context.translation.camera}', onTap: () { Navigator.of(context).pop(ImageSource.camera); }, ), listCard( icon: 'gallery_icon', label: '${context.translation.open}\n${context.translation.gallery}', onTap: () { Navigator.of(context).pop(ImageSource.gallery); }, ), listCard( icon: 'file_icon', label: '${context.translation.open}\n${context.translation.files}', onTap: () async { await fromFilePicker(); Navigator.pop(context); }, ), ]; return SafeArea( top: false, child: Container( width: double.infinity, color: AppColor.bottomSheetColor(context), padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.pickerTitle.heading4(context), GridView.builder( padding: const EdgeInsets.only(top: 16, bottom: 0), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 16, mainAxisSpacing: 16), itemCount: listItems.length, itemBuilder: (context, index) { // Calculate staggered animation delay final delay = index * 0.08; final animation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: Interval( delay.clamp(0.0, 0.8), (delay + 0.3).clamp(0.0, 1.0), curve: Curves.easeOut, ), ), ); final slideAnimation = Tween( begin: const Offset(0.0, 0.3), end: Offset.zero, ).animate( CurvedAnimation( parent: _animationController, curve: Interval( delay.clamp(0.0, 0.8), (delay + 0.3).clamp(0.0, 1.0), curve: Curves.easeOutCubic, ), ), ); return FadeTransition( opacity: animation, child: SlideTransition(position: slideAnimation, child: listItems[index]), ); }, ), ], ), ), ); }, ); if (source == null) return; if (widget.pickMultiple && source == ImageSource.gallery) { final pickedFiles = await ImagePicker().pickMultiImage(imageQuality: 70); if (pickedFiles.isNotEmpty) { pickedFiles.forEach((pickedFile) { File fileImage = File(pickedFile.path); widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType)); }); if (widget.onChange != null) { widget.onChange!(widget.attachment); } setState(() {}); } return; } final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 50); if (pickedFile != null) { File fileImage = File(pickedFile.path); if (checkQuality) { ImageQualityValidator imageQualityValidator = ImageQualityValidator(); final result = await imageQualityValidator.validate(await fileImage.readAsBytes()); if (!result.isValid) { String issues = "Your image have following problems:"; result.issues.forEach((issue) => (issues += "\n- $issue")); context.showInfoDialog(issues, title: "Bad Quality Found"); return; } } widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType)); if (widget.onChange != null) { widget.onChange!(widget.attachment); } setState(() {}); } // setState(() {}); } }