import 'dart:io'; import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../constants/colors.dart'; import '../constants/variables.dart'; import 'spaces.dart'; class ImagePickerWidget extends StatefulWidget { final String label; final void Function(XFile? file) onChanged; final void Function(String? uploadedUrl)? onUploaded; final bool showLabel; final String? initialImageUrl; final bool autoUpload; const ImagePickerWidget({ super.key, required this.label, required this.onChanged, this.onUploaded, this.showLabel = true, this.initialImageUrl, this.autoUpload = false, }); @override State createState() => _ImagePickerWidgetState(); } class _ImagePickerWidgetState extends State with TickerProviderStateMixin { String? imagePath; String? uploadedImageUrl; bool hasInitialImage = false; bool isHovering = false; bool isUploading = false; late AnimationController _scaleController; late AnimationController _fadeController; late AnimationController _uploadController; late Animation _scaleAnimation; late Animation _fadeAnimation; late Animation _uploadAnimation; @override void initState() { super.initState(); hasInitialImage = widget.initialImageUrl != null; _scaleController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _fadeController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _uploadController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.95, ).animate(CurvedAnimation( parent: _scaleController, curve: Curves.easeInOut, )); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut, )); _uploadAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _uploadController, curve: Curves.easeInOut, )); _fadeController.forward(); } @override void dispose() { _scaleController.dispose(); _fadeController.dispose(); _uploadController.dispose(); super.dispose(); } Future _pickImage() async { _scaleController.forward().then((_) { _scaleController.reverse(); }); final pickedFile = await ImagePicker().pickImage( source: ImageSource.gallery, ); if (pickedFile != null) { setState(() { imagePath = pickedFile.path; hasInitialImage = false; uploadedImageUrl = null; }); widget.onChanged(pickedFile); // Auto upload if enabled if (widget.autoUpload) { _uploadImage(pickedFile.path); } } else { debugPrint('No image selected.'); widget.onChanged(null); } } void _uploadImage(String filePath) { setState(() { isUploading = true; }); _uploadController.forward(); context.read().add( UploadFileEvent.upload(filePath), ); } Widget _buildImageContainer() { return Container( width: 100.0, height: 100.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.0), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(20.0), child: Stack( children: [ Positioned.fill( child: imagePath != null ? Image.file( File(imagePath!), fit: BoxFit.cover, ) : uploadedImageUrl != null ? CachedNetworkImage( imageUrl: uploadedImageUrl!.contains('http') ? uploadedImageUrl! : '${Variables.baseUrl}/$uploadedImageUrl', placeholder: (context, url) => Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.1), AppColors.primary.withOpacity(0.05), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: const Center( child: CircularProgressIndicator(strokeWidth: 2), ), ), errorWidget: (context, url, error) => _buildPlaceholder(), fit: BoxFit.cover, ) : hasInitialImage && widget.initialImageUrl != null ? CachedNetworkImage( imageUrl: widget.initialImageUrl!.contains('http') ? widget.initialImageUrl! : '${Variables.baseUrl}/${widget.initialImageUrl}', placeholder: (context, url) => Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.1), AppColors.primary.withOpacity(0.05), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: const Center( child: CircularProgressIndicator(strokeWidth: 2), ), ), errorWidget: (context, url, error) => _buildPlaceholder(), fit: BoxFit.cover, ) : _buildPlaceholder(), ), // Upload progress overlay if (isUploading) Positioned.fill( child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 24, height: 24, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, value: _uploadAnimation.value == 1.0 ? null : _uploadAnimation.value, ), ), const SizedBox(height: 8), const Text( 'Uploading...', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), // Overlay gradient for better button visibility if ((imagePath != null || uploadedImageUrl != null || (hasInitialImage && widget.initialImageUrl != null)) && !isUploading) Positioned.fill( child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, Colors.black.withOpacity(0.3), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ), ], ), ), ); } Widget _buildPlaceholder() { return Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.1), AppColors.primary.withOpacity(0.05), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.add_photo_alternate_outlined, size: 32, color: AppColors.primary.withOpacity(0.6), ), const SizedBox(height: 4), Text( 'Photo', style: TextStyle( fontSize: 10, color: AppColors.primary.withOpacity(0.6), fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _buildActionButton() { bool hasImage = imagePath != null || uploadedImageUrl != null || (hasInitialImage && widget.initialImageUrl != null); return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: LinearGradient( colors: [ AppColors.primary, AppColors.primary.withOpacity(0.8), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: isUploading ? null : _pickImage, onHover: (hover) { setState(() { isHovering = hover; }); }, borderRadius: BorderRadius.circular(12), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: isHovering ? Colors.white.withOpacity(0.1) : Colors.transparent, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isUploading ? Icons.cloud_upload_outlined : hasImage ? Icons.edit_outlined : Icons.add_photo_alternate_outlined, color: Colors.white, size: 18, ), const SizedBox(width: 8), Text( isUploading ? 'Uploading...' : hasImage ? 'Change Photo' : 'Choose Photo', style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ); } Widget _buildUploadButton() { if (!widget.autoUpload && imagePath != null && uploadedImageUrl == null && !isUploading) { return Padding( padding: const EdgeInsets.only(top: 12), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: LinearGradient( colors: [ Colors.green.shade600, Colors.green.shade500, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: Colors.green.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _uploadImage(imagePath!), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.cloud_upload_outlined, color: Colors.white, size: 16, ), const SizedBox(width: 8), Text( 'Upload to Server', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ), ); } return const SizedBox.shrink(); } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { state.when( initial: () {}, loading: () { if (!isUploading) { setState(() { isUploading = true; }); _uploadController.repeat(); } }, success: (fileData) { setState(() { isUploading = false; uploadedImageUrl = fileData.fileUrl; }); _uploadController.reset(); if (widget.onUploaded != null) { widget.onUploaded!(fileData.fileUrl); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.check_circle, color: Colors.white), SizedBox(width: 8), Text('Image uploaded successfully!'), ], ), backgroundColor: Colors.green, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), duration: Duration(seconds: 2), ), ); }, error: (message) { setState(() { isUploading = false; }); _uploadController.reset(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.error_outline, color: Colors.white), SizedBox(width: 8), Expanded(child: Text('Upload failed: $message')), ], ), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), duration: Duration(seconds: 3), ), ); }, ); }, child: FadeTransition( opacity: _fadeAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showLabel) ...[ Text( widget.label, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Colors.black87, ), ), const SpaceHeight(16.0), ], ScaleTransition( scale: _scaleAnimation, child: Container( padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24.0), color: Colors.white, border: Border.all( color: isUploading ? Colors.orange.withOpacity(0.5) : uploadedImageUrl != null ? Colors.green.withOpacity(0.5) : AppColors.primary.withOpacity(0.2), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( children: [ _buildImageContainer(), const SizedBox(height: 20), _buildActionButton(), _buildUploadButton(), if ((imagePath != null || uploadedImageUrl != null || (hasInitialImage && widget.initialImageUrl != null)) && !isUploading) ...[ const SizedBox(height: 12), TextButton.icon( onPressed: () { setState(() { imagePath = null; hasInitialImage = false; uploadedImageUrl = null; }); widget.onChanged(null); if (widget.onUploaded != null) { widget.onUploaded!(null); } }, icon: Icon( Icons.delete_outline, size: 16, color: Colors.red.shade400, ), label: Text( 'Remove Photo', style: TextStyle( color: Colors.red.shade400, fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ], // Upload status indicator if (uploadedImageUrl != null && !isUploading) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.green.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.cloud_done_outlined, size: 14, color: Colors.green.shade600, ), const SizedBox(width: 6), Text( 'Uploaded', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Colors.green.shade600, ), ), ], ), ), ], ], ), ), ), ], ), ), ); } } /// Cara menggunakan widget ini: /// /// ```dart /// // 1. Basic usage tanpa auto upload /// ImagePickerWidget( /// label: 'Product Image', /// onChanged: (file) { /// // Handle selected file /// print('Selected file: ${file?.path}'); /// }, /// onUploaded: (url) { /// // Handle uploaded URL /// print('Uploaded URL: $url'); /// }, /// ) /// /// // 2. Auto upload setelah memilih gambar /// ImagePickerWidget( /// label: 'Profile Picture', /// autoUpload: true, /// onChanged: (file) => setState(() => selectedFile = file), /// onUploaded: (url) => setState(() => profileImageUrl = url), /// ) /// /// // 3. Dengan initial image /// ImagePickerWidget( /// label: 'Banner Image', /// initialImageUrl: existingImageUrl, /// onChanged: (file) => handleFileChange(file), /// onUploaded: (url) => handleUploadSuccess(url), /// ) /// ``` /// /// Pastikan untuk wrap widget ini dengan BlocProvider: /// ```dart /// BlocProvider( /// create: (context) => UploadFileBloc( /// context.read(), /// ), /// child: ImagePickerWidget(...), /// ) /// ```