apskel-pos-flutter/lib/core/components/image_picker_widget.dart

683 lines
22 KiB
Dart
Raw Normal View History

2025-07-30 22:38:44 +07:00
import 'dart:io';
2025-08-05 19:20:45 +07:00
import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart';
2025-07-30 22:38:44 +07:00
import 'package:flutter/material.dart';
2025-08-05 19:20:45 +07:00
import 'package:flutter_bloc/flutter_bloc.dart';
2025-07-30 22:38:44 +07:00
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;
2025-08-05 19:20:45 +07:00
final void Function(String? uploadedUrl)? onUploaded;
2025-07-30 22:38:44 +07:00
final bool showLabel;
final String? initialImageUrl;
2025-08-05 19:20:45 +07:00
final bool autoUpload;
2025-07-30 22:38:44 +07:00
const ImagePickerWidget({
super.key,
required this.label,
required this.onChanged,
2025-08-05 19:20:45 +07:00
this.onUploaded,
2025-07-30 22:38:44 +07:00
this.showLabel = true,
this.initialImageUrl,
2025-08-05 19:20:45 +07:00
this.autoUpload = false,
2025-07-30 22:38:44 +07:00
});
@override
State<ImagePickerWidget> createState() => _ImagePickerWidgetState();
}
2025-08-05 19:20:45 +07:00
class _ImagePickerWidgetState extends State<ImagePickerWidget>
with TickerProviderStateMixin {
2025-07-30 22:38:44 +07:00
String? imagePath;
2025-08-05 19:20:45 +07:00
String? uploadedImageUrl;
2025-07-30 22:38:44 +07:00
bool hasInitialImage = false;
2025-08-05 19:20:45 +07:00
bool isHovering = false;
bool isUploading = false;
late AnimationController _scaleController;
late AnimationController _fadeController;
late AnimationController _uploadController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _uploadAnimation;
2025-07-30 22:38:44 +07:00
@override
void initState() {
super.initState();
hasInitialImage = widget.initialImageUrl != null;
2025-08-05 19:20:45 +07:00
_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<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.easeInOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_uploadAnimation = Tween<double>(
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();
2025-07-30 22:38:44 +07:00
}
Future<void> _pickImage() async {
2025-08-05 19:20:45 +07:00
_scaleController.forward().then((_) {
_scaleController.reverse();
});
2025-07-30 22:38:44 +07:00
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.gallery,
);
2025-08-05 19:20:45 +07:00
if (pickedFile != null) {
setState(() {
2025-07-30 22:38:44 +07:00
imagePath = pickedFile.path;
2025-08-05 19:20:45 +07:00
hasInitialImage = false;
uploadedImageUrl = null;
});
widget.onChanged(pickedFile);
// Auto upload if enabled
if (widget.autoUpload) {
_uploadImage(pickedFile.path);
2025-07-30 22:38:44 +07:00
}
2025-08-05 19:20:45 +07:00
} else {
debugPrint('No image selected.');
widget.onChanged(null);
}
}
void _uploadImage(String filePath) {
setState(() {
isUploading = true;
2025-07-30 22:38:44 +07:00
});
2025-08-05 19:20:45 +07:00
_uploadController.forward();
context.read<UploadFileBloc>().add(
UploadFileEvent.upload(filePath),
);
2025-07-30 22:38:44 +07:00
}
2025-08-05 19:20:45 +07:00
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),
2025-07-30 22:38:44 +07:00
),
],
2025-08-05 19:20:45 +07:00
),
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(),
2025-07-30 22:38:44 +07:00
fit: BoxFit.cover,
)
: hasInitialImage && widget.initialImageUrl != null
? CachedNetworkImage(
imageUrl: widget.initialImageUrl!.contains('http')
? widget.initialImageUrl!
: '${Variables.baseUrl}/${widget.initialImageUrl}',
2025-08-05 19:20:45 +07:00
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),
),
2025-07-30 22:38:44 +07:00
),
2025-08-05 19:20:45 +07:00
errorWidget: (context, url, error) =>
_buildPlaceholder(),
2025-07-30 22:38:44 +07:00
fit: BoxFit.cover,
)
2025-08-05 19:20:45 +07:00
: _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,
),
),
],
),
),
2025-07-30 22:38:44 +07:00
),
),
2025-08-05 19:20:45 +07:00
// 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,
),
),
2025-07-30 22:38:44 +07:00
),
),
2025-08-05 19:20:45 +07:00
],
),
),
);
}
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),
),
2025-07-30 22:38:44 +07:00
],
),
2025-08-05 19:20:45 +07:00
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,
),
),
],
),
),
),
),
2025-07-30 22:38:44 +07:00
),
2025-08-05 19:20:45 +07:00
);
}
return const SizedBox.shrink();
}
@override
Widget build(BuildContext context) {
return BlocListener<UploadFileBloc, UploadFileState>(
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,
),
),
],
),
),
],
],
),
),
),
],
),
),
2025-07-30 22:38:44 +07:00
);
}
}
2025-08-05 19:20:45 +07:00
/// 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<FileRemoteDataSource>(),
/// ),
/// child: ImagePickerWidget(...),
/// )
/// ```