baseproject
This commit is contained in:
357
base_project/lib/shared/widgets/inputs/modern_image_picker.dart
Normal file
357
base_project/lib/shared/widgets/inputs/modern_image_picker.dart
Normal file
@@ -0,0 +1,357 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
import '../buttons/modern_button.dart';
|
||||
|
||||
class ModernImagePicker extends StatefulWidget {
|
||||
final String? label;
|
||||
final String? hint;
|
||||
final Uint8List? imageBytes;
|
||||
final File? imageFile;
|
||||
final String? imageUrl;
|
||||
final VoidCallback? onPickImage;
|
||||
final VoidCallback? onRemoveImage;
|
||||
final bool isLoading;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final BorderRadius? borderRadius;
|
||||
final String? errorText;
|
||||
final bool showRemoveButton;
|
||||
final IconData? placeholderIcon;
|
||||
final String? placeholderText;
|
||||
|
||||
const ModernImagePicker({
|
||||
super.key,
|
||||
this.label,
|
||||
this.hint,
|
||||
this.imageBytes,
|
||||
this.imageFile,
|
||||
this.imageUrl,
|
||||
this.onPickImage,
|
||||
this.onRemoveImage,
|
||||
this.isLoading = false,
|
||||
this.height = 200,
|
||||
this.width,
|
||||
this.borderRadius,
|
||||
this.errorText,
|
||||
this.showRemoveButton = true,
|
||||
this.placeholderIcon,
|
||||
this.placeholderText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ModernImagePicker> createState() => _ModernImagePickerState();
|
||||
}
|
||||
|
||||
class _ModernImagePickerState extends State<ModernImagePicker>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: UIConstants.durationFast,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveNormal,
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.8,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveNormal,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get hasImage =>
|
||||
widget.imageBytes != null ||
|
||||
widget.imageFile != null ||
|
||||
(widget.imageUrl != null && widget.imageUrl!.isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label
|
||||
if (widget.label != null) ...[
|
||||
Text(
|
||||
widget.label!,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
],
|
||||
|
||||
// Image Container
|
||||
GestureDetector(
|
||||
onTapDown: (_) => _animationController.forward(),
|
||||
onTapUp: (_) => _animationController.reverse(),
|
||||
onTapCancel: () => _animationController.reverse(),
|
||||
onTap: widget.onPickImage,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Container(
|
||||
height: widget.height,
|
||||
width: widget.width ?? double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: widget.borderRadius ??
|
||||
BorderRadius.circular(UIConstants.radius16),
|
||||
border: Border.all(
|
||||
color: widget.errorText != null
|
||||
? colorScheme.error
|
||||
: colorScheme.outline.withOpacity(0.3),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: widget.borderRadius ??
|
||||
BorderRadius.circular(UIConstants.radius16),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image or Placeholder
|
||||
if (hasImage)
|
||||
_buildImageWidget()
|
||||
else
|
||||
_buildPlaceholder(theme, colorScheme),
|
||||
|
||||
// Loading Overlay
|
||||
if (widget.isLoading)
|
||||
_buildLoadingOverlay(colorScheme),
|
||||
|
||||
// Remove Button
|
||||
if (hasImage &&
|
||||
widget.showRemoveButton &&
|
||||
widget.onRemoveImage != null)
|
||||
_buildRemoveButton(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Hint Text
|
||||
if (widget.hint != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
widget.hint!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Error Text
|
||||
if (widget.errorText != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
widget.errorText!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Action Buttons
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ModernButton(
|
||||
text: hasImage ? 'Change Image' : 'Pick Image',
|
||||
type: ModernButtonType.outline,
|
||||
size: ModernButtonSize.medium,
|
||||
icon: Icon(hasImage ? Icons.edit : Icons.add_photo_alternate),
|
||||
onPressed: widget.isLoading ? null : widget.onPickImage,
|
||||
isLoading: widget.isLoading,
|
||||
),
|
||||
),
|
||||
if (hasImage && widget.onRemoveImage != null) ...[
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
ModernButton(
|
||||
text: 'Remove',
|
||||
type: ModernButtonType.danger,
|
||||
size: ModernButtonSize.medium,
|
||||
icon: Icon(Icons.delete_outline),
|
||||
onPressed: widget.isLoading ? null : widget.onRemoveImage,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageWidget() {
|
||||
if (widget.imageBytes != null) {
|
||||
return Image.memory(
|
||||
widget.imageBytes!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else if (widget.imageFile != null) {
|
||||
return Image.file(
|
||||
widget.imageFile!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else if (widget.imageUrl != null && widget.imageUrl!.isNotEmpty) {
|
||||
return Image.network(
|
||||
widget.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildPlaceholder(
|
||||
Theme.of(context), Theme.of(context).colorScheme);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder(ThemeData theme, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
widget.placeholderIcon ?? Icons.add_photo_alternate,
|
||||
size: UIConstants.iconSizeXLarge,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
Text(
|
||||
widget.placeholderText ?? 'Tap to select image',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
'JPG, PNG, GIF up to 10MB',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingOverlay(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: colorScheme.surface.withOpacity(0.8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
Text(
|
||||
'Uploading...',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRemoveButton(ColorScheme colorScheme) {
|
||||
return Positioned(
|
||||
top: UIConstants.spacing8,
|
||||
right: UIConstants.spacing8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.error,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: widget.onRemoveImage,
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: colorScheme.onError,
|
||||
size: UIConstants.iconSizeSmall,
|
||||
),
|
||||
padding: const EdgeInsets.all(UIConstants.spacing4),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
base_project/lib/shared/widgets/inputs/modern_text_field.dart
Normal file
298
base_project/lib/shared/widgets/inputs/modern_text_field.dart
Normal file
@@ -0,0 +1,298 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../../core/constants/ui_constants.dart';
|
||||
|
||||
class ModernTextField extends StatefulWidget {
|
||||
final String? label;
|
||||
final String? hint;
|
||||
final String? helperText;
|
||||
final String? errorText;
|
||||
final TextEditingController? controller;
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
final int? maxLines;
|
||||
final int? maxLength;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final VoidCallback? onSuffixIconPressed;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function(String)? onSubmitted;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final FocusNode? focusNode;
|
||||
final VoidCallback? onTap;
|
||||
final bool autofocus;
|
||||
final TextCapitalization textCapitalization;
|
||||
final TextInputAction? textInputAction;
|
||||
final bool expands;
|
||||
final double? height;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
const ModernTextField({
|
||||
super.key,
|
||||
this.label,
|
||||
this.hint,
|
||||
this.helperText,
|
||||
this.errorText,
|
||||
this.controller,
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.maxLines = 1,
|
||||
this.maxLength,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.onSuffixIconPressed,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.inputFormatters,
|
||||
this.focusNode,
|
||||
this.onTap,
|
||||
this.autofocus = false,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.textInputAction,
|
||||
this.expands = false,
|
||||
this.height,
|
||||
this.contentPadding,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ModernTextField> createState() => _ModernTextFieldState();
|
||||
}
|
||||
|
||||
class _ModernTextFieldState extends State<ModernTextField>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isFocused = false;
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: UIConstants.durationFast,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveFast,
|
||||
));
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 0.95,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: UIConstants.curveFast,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFocusChange(bool hasFocus) {
|
||||
setState(() {
|
||||
_isFocused = hasFocus;
|
||||
});
|
||||
|
||||
if (hasFocus) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _onChanged(String value) {
|
||||
setState(() {
|
||||
_hasError = false;
|
||||
});
|
||||
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.label != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
|
||||
child: Text(
|
||||
widget.label!,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: _isFocused
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Focus(
|
||||
onFocusChange: _onFocusChange,
|
||||
child: Container(
|
||||
height: widget.height ?? UIConstants.inputHeightMedium,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||||
color: _isFocused
|
||||
? colorScheme.surfaceVariant.withOpacity(0.5)
|
||||
: colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
border: Border.all(
|
||||
color: _hasError
|
||||
? colorScheme.error
|
||||
: _isFocused
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline.withOpacity(0.3),
|
||||
width: _isFocused ? 2.0 : 1.5,
|
||||
),
|
||||
boxShadow: _isFocused
|
||||
? [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
obscureText: widget.obscureText,
|
||||
enabled: widget.enabled,
|
||||
readOnly: widget.readOnly,
|
||||
maxLines: widget.maxLines,
|
||||
maxLength: widget.maxLength,
|
||||
autofocus: widget.autofocus,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
textInputAction: widget.textInputAction,
|
||||
expands: widget.expands,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
validator: (value) {
|
||||
if (widget.validator != null) {
|
||||
final result = widget.validator!(value);
|
||||
setState(() {
|
||||
_hasError = result != null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _onChanged,
|
||||
onFieldSubmitted: widget.onSubmitted,
|
||||
onTap: widget.onTap,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
hintStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
),
|
||||
prefixIcon: widget.prefixIcon != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: UIConstants.spacing16),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: _isFocused
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: UIConstants.iconSizeMedium,
|
||||
),
|
||||
child: widget.prefixIcon!,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: UIConstants.spacing16),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: _isFocused
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: UIConstants.iconSizeMedium,
|
||||
),
|
||||
child: widget.suffixIcon!,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
contentPadding: widget.contentPadding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing20,
|
||||
vertical: UIConstants.spacing16,
|
||||
),
|
||||
counterText: '',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.helperText != null && !_hasError) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UIConstants.spacing8),
|
||||
child: Text(
|
||||
widget.helperText!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.errorText != null && _hasError) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UIConstants.spacing8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: UIConstants.iconSizeSmall,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.errorText!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user