Refactoring
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
/// Widget de carte unifié pour toute l'application
|
||||
///
|
||||
/// Fournit un design cohérent avec :
|
||||
/// - Styles standardisés (élévation, bordures, couleurs)
|
||||
/// - Support des animations hover et tap
|
||||
/// - Variantes de style (elevated, outlined, filled)
|
||||
/// - Gestion des états (loading, disabled)
|
||||
class UnifiedCard extends StatefulWidget {
|
||||
/// Contenu principal de la carte
|
||||
final Widget child;
|
||||
|
||||
/// Callback lors du tap sur la carte
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Callback lors du long press
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Padding interne de la carte
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// Marge externe de la carte
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
/// Largeur de la carte
|
||||
final double? width;
|
||||
|
||||
/// Hauteur de la carte
|
||||
final double? height;
|
||||
|
||||
/// Variante de style de la carte
|
||||
final UnifiedCardVariant variant;
|
||||
|
||||
/// Couleur de fond personnalisée
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Couleur de bordure personnalisée
|
||||
final Color? borderColor;
|
||||
|
||||
/// Indique si la carte est désactivée
|
||||
final bool disabled;
|
||||
|
||||
/// Indique si la carte est en cours de chargement
|
||||
final bool loading;
|
||||
|
||||
/// Élévation personnalisée
|
||||
final double? elevation;
|
||||
|
||||
/// Rayon des bordures personnalisé
|
||||
final double? borderRadius;
|
||||
|
||||
const UnifiedCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.variant = UnifiedCardVariant.elevated,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
/// Constructeur pour une carte élevée
|
||||
const UnifiedCard.elevated({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
}) : variant = UnifiedCardVariant.elevated,
|
||||
borderColor = null;
|
||||
|
||||
/// Constructeur pour une carte avec bordure
|
||||
const UnifiedCard.outlined({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
}) : variant = UnifiedCardVariant.outlined;
|
||||
|
||||
/// Constructeur pour une carte remplie
|
||||
const UnifiedCard.filled({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
this.elevation,
|
||||
this.borderRadius,
|
||||
}) : variant = UnifiedCardVariant.filled;
|
||||
|
||||
/// Constructeur pour une carte KPI
|
||||
const UnifiedCard.kpi({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
}) : variant = UnifiedCardVariant.elevated,
|
||||
padding = const EdgeInsets.all(20),
|
||||
borderColor = null,
|
||||
elevation = 2,
|
||||
borderRadius = 16;
|
||||
|
||||
/// Constructeur pour une carte de liste
|
||||
const UnifiedCard.listItem({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.backgroundColor,
|
||||
this.disabled = false,
|
||||
this.loading = false,
|
||||
}) : variant = UnifiedCardVariant.outlined,
|
||||
padding = const EdgeInsets.all(16),
|
||||
borderColor = null,
|
||||
elevation = 0,
|
||||
borderRadius = 12;
|
||||
|
||||
@override
|
||||
State<UnifiedCard> createState() => _UnifiedCardState();
|
||||
}
|
||||
|
||||
class _UnifiedCardState extends State<UnifiedCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _elevationAnimation;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.98,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_elevationAnimation = Tween<double>(
|
||||
begin: _getBaseElevation(),
|
||||
end: _getBaseElevation() + 2,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _getBaseElevation() {
|
||||
if (widget.elevation != null) return widget.elevation!;
|
||||
switch (widget.variant) {
|
||||
case UnifiedCardVariant.elevated:
|
||||
return 2;
|
||||
case UnifiedCardVariant.outlined:
|
||||
return 0;
|
||||
case UnifiedCardVariant.filled:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
if (widget.backgroundColor != null) return widget.backgroundColor!;
|
||||
if (widget.disabled) return AppTheme.surfaceVariant.withOpacity(0.5);
|
||||
|
||||
switch (widget.variant) {
|
||||
case UnifiedCardVariant.elevated:
|
||||
return Colors.white;
|
||||
case UnifiedCardVariant.outlined:
|
||||
return Colors.white;
|
||||
case UnifiedCardVariant.filled:
|
||||
return AppTheme.surfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
Border? _getBorder() {
|
||||
if (widget.variant == UnifiedCardVariant.outlined) {
|
||||
return Border.all(
|
||||
color: widget.borderColor ?? AppTheme.outline,
|
||||
width: 1,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget card = AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
margin: widget.margin,
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(),
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
||||
border: _getBorder(),
|
||||
boxShadow: widget.variant == UnifiedCardVariant.elevated
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: _elevationAnimation.value * 2,
|
||||
offset: Offset(0, _elevationAnimation.value),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: widget.loading
|
||||
? _buildLoadingState()
|
||||
: Padding(
|
||||
padding: widget.padding ?? const EdgeInsets.all(16),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.onTap != null && !widget.disabled && !widget.loading) {
|
||||
card = MouseRegion(
|
||||
onEnter: (_) => _onHover(true),
|
||||
onExit: (_) => _onHover(false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
onTapDown: (_) => _animationController.forward(),
|
||||
onTapUp: (_) => _animationController.reverse(),
|
||||
onTapCancel: () => _animationController.reverse(),
|
||||
child: card,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
void _onHover(bool isHovered) {
|
||||
if (mounted && !widget.disabled && !widget.loading) {
|
||||
setState(() {
|
||||
_isHovered = isHovered;
|
||||
});
|
||||
if (isHovered) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
return Container(
|
||||
padding: widget.padding ?? const EdgeInsets.all(16),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Variantes de style pour les cartes unifiées
|
||||
enum UnifiedCardVariant {
|
||||
/// Carte avec élévation et ombre
|
||||
elevated,
|
||||
|
||||
/// Carte avec bordure uniquement
|
||||
outlined,
|
||||
|
||||
/// Carte avec fond coloré
|
||||
filled,
|
||||
}
|
||||
Reference in New Issue
Block a user