This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
/// Custom button widget with loading state and consistent styling
///
/// This widget provides a reusable button component with:
/// - Loading indicator support
/// - Disabled state
/// - Customizable colors, icons, and text
/// - Consistent padding and styling
///
/// Usage:
/// ```dart
/// CustomButton(
/// text: 'Login',
/// onPressed: _handleLogin,
/// isLoading: _isLoading,
/// )
///
/// CustomButton.outlined(
/// text: 'Cancel',
/// onPressed: _handleCancel,
/// )
///
/// CustomButton.text(
/// text: 'Skip',
/// onPressed: _handleSkip,
/// )
/// ```
class CustomButton extends StatelessWidget {
/// Button text
final String text;
/// Callback when button is pressed
final VoidCallback? onPressed;
/// Whether the button is in loading state
final bool isLoading;
/// Optional icon to display before text
final IconData? icon;
/// Button style variant
final ButtonStyle? style;
/// Whether this is an outlined button
final bool isOutlined;
/// Whether this is a text button
final bool isTextButton;
/// Minimum button width (null for full width)
final double? minWidth;
/// Minimum button height
final double? minHeight;
/// Background color (only for elevated buttons)
final Color? backgroundColor;
/// Foreground/text color
final Color? foregroundColor;
/// Border color (only for outlined buttons)
final Color? borderColor;
/// Font size
final double? fontSize;
/// Font weight
final FontWeight? fontWeight;
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
this.icon,
this.style,
this.minWidth,
this.minHeight,
this.backgroundColor,
this.foregroundColor,
this.fontSize,
this.fontWeight,
}) : isOutlined = false,
isTextButton = false,
borderColor = null;
/// Create an outlined button variant
const CustomButton.outlined({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
this.icon,
this.style,
this.minWidth,
this.minHeight,
this.foregroundColor,
this.borderColor,
this.fontSize,
this.fontWeight,
}) : isOutlined = true,
isTextButton = false,
backgroundColor = null;
/// Create a text button variant
const CustomButton.text({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
this.icon,
this.style,
this.minWidth,
this.minHeight,
this.foregroundColor,
this.fontSize,
this.fontWeight,
}) : isOutlined = false,
isTextButton = true,
backgroundColor = null,
borderColor = null;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// Determine if button should be disabled
final bool isDisabled = onPressed == null || isLoading;
// Build button content
Widget content;
if (isLoading) {
content = SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isTextButton
? foregroundColor ?? colorScheme.primary
: foregroundColor ?? colorScheme.onPrimary,
),
),
);
} else if (icon != null) {
content = Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20),
const SizedBox(width: 8),
Flexible(
child: Text(
text,
overflow: TextOverflow.ellipsis,
),
),
],
);
} else {
content = Text(text);
}
// Build button style
final ButtonStyle buttonStyle = style ??
(isTextButton
? _buildTextButtonStyle(context)
: isOutlined
? _buildOutlinedButtonStyle(context)
: _buildElevatedButtonStyle(context));
// Build appropriate button widget
if (isTextButton) {
return TextButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: content,
);
} else if (isOutlined) {
return OutlinedButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: content,
);
} else {
return ElevatedButton(
onPressed: isDisabled ? null : onPressed,
style: buttonStyle,
child: content,
);
}
}
/// Build elevated button style
ButtonStyle _buildElevatedButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? colorScheme.primary,
foregroundColor: foregroundColor ?? colorScheme.onPrimary,
minimumSize: Size(
minWidth ?? double.infinity,
minHeight ?? 48,
),
textStyle: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: fontWeight ?? FontWeight.w600,
),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
);
}
/// Build outlined button style
ButtonStyle _buildOutlinedButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return OutlinedButton.styleFrom(
foregroundColor: foregroundColor ?? colorScheme.primary,
side: BorderSide(
color: borderColor ?? colorScheme.primary,
width: 1.5,
),
minimumSize: Size(
minWidth ?? double.infinity,
minHeight ?? 48,
),
textStyle: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: fontWeight ?? FontWeight.w600,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
);
}
/// Build text button style
ButtonStyle _buildTextButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return TextButton.styleFrom(
foregroundColor: foregroundColor ?? colorScheme.primary,
minimumSize: Size(
minWidth ?? 0,
minHeight ?? 48,
),
textStyle: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: fontWeight ?? FontWeight.w600,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
);
}
}
/// Icon button with loading state
class CustomIconButton extends StatelessWidget {
/// Icon to display
final IconData icon;
/// Callback when button is pressed
final VoidCallback? onPressed;
/// Whether the button is in loading state
final bool isLoading;
/// Icon size
final double? iconSize;
/// Icon color
final Color? color;
/// Background color
final Color? backgroundColor;
/// Tooltip text
final String? tooltip;
const CustomIconButton({
super.key,
required this.icon,
required this.onPressed,
this.isLoading = false,
this.iconSize,
this.color,
this.backgroundColor,
this.tooltip,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bool isDisabled = onPressed == null || isLoading;
Widget button = IconButton(
icon: isLoading
? SizedBox(
height: iconSize ?? 24,
width: iconSize ?? 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
color ?? theme.colorScheme.primary,
),
),
)
: Icon(
icon,
size: iconSize ?? 24,
color: color,
),
onPressed: isDisabled ? null : onPressed,
style: backgroundColor != null
? IconButton.styleFrom(
backgroundColor: backgroundColor,
)
: null,
);
if (tooltip != null) {
return Tooltip(
message: tooltip!,
child: button,
);
}
return button;
}
}