Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/shared/widgets/buttons/unified_button_set.dart
2025-09-17 17:54:06 +00:00

412 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
/// Ensemble de boutons unifiés pour toute l'application
///
/// Fournit des styles cohérents pour :
/// - Boutons primaires, secondaires, tertiaires
/// - Boutons d'action (success, warning, error)
/// - Boutons avec icônes
/// - États de chargement et désactivé
class UnifiedButton extends StatefulWidget {
/// Texte du bouton
final String text;
/// Icône optionnelle
final IconData? icon;
/// Position de l'icône
final UnifiedButtonIconPosition iconPosition;
/// Callback lors du tap
final VoidCallback? onPressed;
/// Style du bouton
final UnifiedButtonStyle style;
/// Taille du bouton
final UnifiedButtonSize size;
/// Indique si le bouton est en cours de chargement
final bool isLoading;
/// Indique si le bouton prend toute la largeur disponible
final bool fullWidth;
/// Couleur personnalisée
final Color? customColor;
const UnifiedButton({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.style = UnifiedButtonStyle.primary,
this.size = UnifiedButtonSize.medium,
this.isLoading = false,
this.fullWidth = false,
this.customColor,
});
/// Constructeur pour bouton primaire
const UnifiedButton.primary({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.size = UnifiedButtonSize.medium,
this.isLoading = false,
this.fullWidth = false,
}) : style = UnifiedButtonStyle.primary,
customColor = null;
/// Constructeur pour bouton secondaire
const UnifiedButton.secondary({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.size = UnifiedButtonSize.medium,
this.isLoading = false,
this.fullWidth = false,
}) : style = UnifiedButtonStyle.secondary,
customColor = null;
/// Constructeur pour bouton tertiaire
const UnifiedButton.tertiary({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.isLoading = false,
this.size = UnifiedButtonSize.medium,
this.fullWidth = false,
}) : style = UnifiedButtonStyle.tertiary,
customColor = null;
/// Constructeur pour bouton de succès
const UnifiedButton.success({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.size = UnifiedButtonSize.medium,
this.isLoading = false,
this.fullWidth = false,
}) : style = UnifiedButtonStyle.success,
customColor = null;
/// Constructeur pour bouton d'erreur
const UnifiedButton.error({
super.key,
required this.text,
this.icon,
this.iconPosition = UnifiedButtonIconPosition.left,
this.onPressed,
this.size = UnifiedButtonSize.medium,
this.isLoading = false,
this.fullWidth = false,
}) : style = UnifiedButtonStyle.error,
customColor = null;
@override
State<UnifiedButton> createState() => _UnifiedButtonState();
}
class _UnifiedButtonState extends State<UnifiedButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEnabled = widget.onPressed != null && !widget.isLoading;
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: widget.fullWidth ? double.infinity : null,
height: _getButtonHeight(),
child: GestureDetector(
onTapDown: isEnabled ? (_) => _animationController.forward() : null,
onTapUp: isEnabled ? (_) => _animationController.reverse() : null,
onTapCancel: isEnabled ? () => _animationController.reverse() : null,
child: ElevatedButton(
onPressed: isEnabled ? widget.onPressed : null,
style: _getButtonStyle(),
child: widget.isLoading ? _buildLoadingContent() : _buildContent(),
),
),
),
);
},
);
}
double _getButtonHeight() {
switch (widget.size) {
case UnifiedButtonSize.small:
return 36;
case UnifiedButtonSize.medium:
return 44;
case UnifiedButtonSize.large:
return 52;
}
}
ButtonStyle _getButtonStyle() {
final colors = _getColors();
return ElevatedButton.styleFrom(
backgroundColor: colors.background,
foregroundColor: colors.foreground,
disabledBackgroundColor: colors.disabledBackground,
disabledForegroundColor: colors.disabledForeground,
elevation: _getElevation(),
shadowColor: colors.shadow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_getBorderRadius()),
side: _getBorderSide(colors),
),
padding: _getPadding(),
);
}
_ButtonColors _getColors() {
final customColor = widget.customColor;
switch (widget.style) {
case UnifiedButtonStyle.primary:
return _ButtonColors(
background: customColor ?? AppTheme.primaryColor,
foreground: Colors.white,
disabledBackground: AppTheme.surfaceVariant,
disabledForeground: AppTheme.textSecondary,
shadow: (customColor ?? AppTheme.primaryColor).withOpacity(0.3),
);
case UnifiedButtonStyle.secondary:
return _ButtonColors(
background: Colors.white,
foreground: customColor ?? AppTheme.primaryColor,
disabledBackground: AppTheme.surfaceVariant,
disabledForeground: AppTheme.textSecondary,
shadow: Colors.black.withOpacity(0.1),
borderColor: customColor ?? AppTheme.primaryColor,
);
case UnifiedButtonStyle.tertiary:
return _ButtonColors(
background: Colors.transparent,
foreground: customColor ?? AppTheme.primaryColor,
disabledBackground: Colors.transparent,
disabledForeground: AppTheme.textSecondary,
shadow: Colors.transparent,
);
case UnifiedButtonStyle.success:
return _ButtonColors(
background: customColor ?? AppTheme.successColor,
foreground: Colors.white,
disabledBackground: AppTheme.surfaceVariant,
disabledForeground: AppTheme.textSecondary,
shadow: (customColor ?? AppTheme.successColor).withOpacity(0.3),
);
case UnifiedButtonStyle.warning:
return _ButtonColors(
background: customColor ?? AppTheme.warningColor,
foreground: Colors.white,
disabledBackground: AppTheme.surfaceVariant,
disabledForeground: AppTheme.textSecondary,
shadow: (customColor ?? AppTheme.warningColor).withOpacity(0.3),
);
case UnifiedButtonStyle.error:
return _ButtonColors(
background: customColor ?? AppTheme.errorColor,
foreground: Colors.white,
disabledBackground: AppTheme.surfaceVariant,
disabledForeground: AppTheme.textSecondary,
shadow: (customColor ?? AppTheme.errorColor).withOpacity(0.3),
);
}
}
double _getElevation() {
switch (widget.style) {
case UnifiedButtonStyle.primary:
case UnifiedButtonStyle.success:
case UnifiedButtonStyle.warning:
case UnifiedButtonStyle.error:
return 2;
case UnifiedButtonStyle.secondary:
return 1;
case UnifiedButtonStyle.tertiary:
return 0;
}
}
double _getBorderRadius() {
switch (widget.size) {
case UnifiedButtonSize.small:
return 8;
case UnifiedButtonSize.medium:
return 10;
case UnifiedButtonSize.large:
return 12;
}
}
BorderSide _getBorderSide(_ButtonColors colors) {
if (colors.borderColor != null) {
return BorderSide(color: colors.borderColor!, width: 1);
}
return BorderSide.none;
}
EdgeInsetsGeometry _getPadding() {
switch (widget.size) {
case UnifiedButtonSize.small:
return const EdgeInsets.symmetric(horizontal: 12, vertical: 6);
case UnifiedButtonSize.medium:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 8);
case UnifiedButtonSize.large:
return const EdgeInsets.symmetric(horizontal: 20, vertical: 10);
}
}
Widget _buildContent() {
final List<Widget> children = [];
if (widget.icon != null && widget.iconPosition == UnifiedButtonIconPosition.left) {
children.add(Icon(widget.icon, size: _getIconSize()));
children.add(const SizedBox(width: 8));
}
children.add(
Text(
widget.text,
style: TextStyle(
fontSize: _getFontSize(),
fontWeight: FontWeight.w600,
),
),
);
if (widget.icon != null && widget.iconPosition == UnifiedButtonIconPosition.right) {
children.add(const SizedBox(width: 8));
children.add(Icon(widget.icon, size: _getIconSize()));
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: children,
);
}
Widget _buildLoadingContent() {
return SizedBox(
width: _getIconSize(),
height: _getIconSize(),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
_getColors().foreground,
),
),
);
}
double _getIconSize() {
switch (widget.size) {
case UnifiedButtonSize.small:
return 16;
case UnifiedButtonSize.medium:
return 18;
case UnifiedButtonSize.large:
return 20;
}
}
double _getFontSize() {
switch (widget.size) {
case UnifiedButtonSize.small:
return 12;
case UnifiedButtonSize.medium:
return 14;
case UnifiedButtonSize.large:
return 16;
}
}
}
/// Styles de boutons disponibles
enum UnifiedButtonStyle {
primary,
secondary,
tertiary,
success,
warning,
error,
}
/// Tailles de boutons disponibles
enum UnifiedButtonSize {
small,
medium,
large,
}
/// Position de l'icône dans le bouton
enum UnifiedButtonIconPosition {
left,
right,
}
/// Classe pour gérer les couleurs des boutons
class _ButtonColors {
final Color background;
final Color foreground;
final Color disabledBackground;
final Color disabledForeground;
final Color shadow;
final Color? borderColor;
const _ButtonColors({
required this.background,
required this.foreground,
required this.disabledBackground,
required this.disabledForeground,
required this.shadow,
this.borderColor,
});
}