Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,409 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../badges/status_badge.dart';
import '../badges/count_badge.dart';
enum AvatarSize {
tiny,
small,
medium,
large,
extraLarge,
}
enum AvatarShape {
circle,
rounded,
square,
}
enum AvatarVariant {
standard,
gradient,
outlined,
glass,
}
class SophisticatedAvatar extends StatefulWidget {
final String? imageUrl;
final String? initials;
final IconData? icon;
final AvatarSize size;
final AvatarShape shape;
final AvatarVariant variant;
final Color? backgroundColor;
final Color? foregroundColor;
final Gradient? gradient;
final VoidCallback? onTap;
final Widget? badge;
final bool showOnlineStatus;
final bool isOnline;
final Widget? overlay;
final bool animated;
final List<BoxShadow>? customShadow;
final Border? border;
const SophisticatedAvatar({
super.key,
this.imageUrl,
this.initials,
this.icon,
this.size = AvatarSize.medium,
this.shape = AvatarShape.circle,
this.variant = AvatarVariant.standard,
this.backgroundColor,
this.foregroundColor,
this.gradient,
this.onTap,
this.badge,
this.showOnlineStatus = false,
this.isOnline = false,
this.overlay,
this.animated = true,
this.customShadow,
this.border,
});
@override
State<SophisticatedAvatar> createState() => _SophisticatedAvatarState();
}
class _SophisticatedAvatarState extends State<SophisticatedAvatar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.1,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = _getSize();
final borderRadius = _getBorderRadius(size);
Widget avatar = AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: widget.animated ? _scaleAnimation.value : 1.0,
child: Transform.rotate(
angle: widget.animated ? _rotationAnimation.value : 0.0,
child: Container(
width: size,
height: size,
decoration: _getDecoration(size, borderRadius),
child: ClipRRect(
borderRadius: borderRadius,
child: Stack(
fit: StackFit.expand,
children: [
_buildContent(),
if (widget.overlay != null) widget.overlay!,
],
),
),
),
),
);
},
);
// Wrap with gesture detector if onTap is provided
if (widget.onTap != null) {
avatar = GestureDetector(
onTap: widget.onTap,
onTapDown: widget.animated ? (_) => _animationController.forward() : null,
onTapUp: widget.animated ? (_) => _animationController.reverse() : null,
onTapCancel: widget.animated ? () => _animationController.reverse() : null,
child: avatar,
);
}
// Add badges and status indicators
return Stack(
clipBehavior: Clip.none,
children: [
avatar,
// Online status indicator
if (widget.showOnlineStatus)
Positioned(
bottom: size * 0.05,
right: size * 0.05,
child: Container(
width: size * 0.25,
height: size * 0.25,
decoration: BoxDecoration(
color: widget.isOnline ? AppTheme.successColor : AppTheme.textHint,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: size * 0.02,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
),
),
// Custom badge
if (widget.badge != null)
Positioned(
top: -size * 0.1,
right: -size * 0.1,
child: widget.badge!,
),
],
);
}
double _getSize() {
switch (widget.size) {
case AvatarSize.tiny:
return 24;
case AvatarSize.small:
return 32;
case AvatarSize.medium:
return 48;
case AvatarSize.large:
return 64;
case AvatarSize.extraLarge:
return 96;
}
}
BorderRadius _getBorderRadius(double size) {
switch (widget.shape) {
case AvatarShape.circle:
return BorderRadius.circular(size / 2);
case AvatarShape.rounded:
return BorderRadius.circular(size * 0.2);
case AvatarShape.square:
return BorderRadius.zero;
}
}
double _getFontSize() {
switch (widget.size) {
case AvatarSize.tiny:
return 10;
case AvatarSize.small:
return 12;
case AvatarSize.medium:
return 18;
case AvatarSize.large:
return 24;
case AvatarSize.extraLarge:
return 36;
}
}
double _getIconSize() {
switch (widget.size) {
case AvatarSize.tiny:
return 12;
case AvatarSize.small:
return 16;
case AvatarSize.medium:
return 24;
case AvatarSize.large:
return 32;
case AvatarSize.extraLarge:
return 48;
}
}
Decoration _getDecoration(double size, BorderRadius borderRadius) {
switch (widget.variant) {
case AvatarVariant.standard:
return BoxDecoration(
color: widget.backgroundColor ?? AppTheme.primaryColor,
borderRadius: borderRadius,
border: widget.border,
boxShadow: widget.customShadow ?? [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
);
case AvatarVariant.gradient:
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [
widget.backgroundColor ?? AppTheme.primaryColor,
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: borderRadius,
border: widget.border,
boxShadow: [
BoxShadow(
color: (widget.backgroundColor ?? AppTheme.primaryColor)
.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 6),
),
],
);
case AvatarVariant.outlined:
return BoxDecoration(
color: Colors.transparent,
borderRadius: borderRadius,
border: widget.border ?? Border.all(
color: widget.backgroundColor ?? AppTheme.primaryColor,
width: 2,
),
);
case AvatarVariant.glass:
return BoxDecoration(
color: (widget.backgroundColor ?? Colors.white).withOpacity(0.2),
borderRadius: borderRadius,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
);
}
}
Widget _buildContent() {
final foregroundColor = widget.foregroundColor ?? Colors.white;
if (widget.imageUrl != null && widget.imageUrl!.isNotEmpty) {
return Image.network(
widget.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _buildFallback(foregroundColor),
);
}
return _buildFallback(foregroundColor);
}
Widget _buildFallback(Color foregroundColor) {
if (widget.initials != null && widget.initials!.isNotEmpty) {
return Center(
child: Text(
widget.initials!.toUpperCase(),
style: TextStyle(
color: foregroundColor,
fontSize: _getFontSize(),
fontWeight: FontWeight.bold,
),
),
);
}
if (widget.icon != null) {
return Center(
child: Icon(
widget.icon,
color: foregroundColor,
size: _getIconSize(),
),
);
}
return Center(
child: Icon(
Icons.person,
color: foregroundColor,
size: _getIconSize(),
),
);
}
}
// Predefined avatar variants
class CircleAvatar extends SophisticatedAvatar {
const CircleAvatar({
super.key,
super.imageUrl,
super.initials,
super.icon,
super.size,
super.backgroundColor,
super.foregroundColor,
super.onTap,
super.badge,
super.showOnlineStatus,
super.isOnline,
}) : super(shape: AvatarShape.circle);
}
class RoundedAvatar extends SophisticatedAvatar {
const RoundedAvatar({
super.key,
super.imageUrl,
super.initials,
super.icon,
super.size,
super.backgroundColor,
super.foregroundColor,
super.onTap,
super.badge,
}) : super(shape: AvatarShape.rounded);
}
class GradientAvatar extends SophisticatedAvatar {
const GradientAvatar({
super.key,
super.imageUrl,
super.initials,
super.icon,
super.size,
super.gradient,
super.onTap,
super.badge,
super.showOnlineStatus,
super.isOnline,
}) : super(variant: AvatarVariant.gradient);
}

View File

@@ -1,202 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
class CountBadge extends StatefulWidget {
final int count;
final Color? backgroundColor;
final Color? textColor;
final double? size;
final bool showZero;
final bool animated;
final String? suffix;
final int? maxCount;
final VoidCallback? onTap;
const CountBadge({
super.key,
required this.count,
this.backgroundColor,
this.textColor,
this.size,
this.showZero = false,
this.animated = true,
this.suffix,
this.maxCount,
this.onTap,
});
@override
State<CountBadge> createState() => _CountBadgeState();
}
class _CountBadgeState extends State<CountBadge>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _bounceAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.elasticOut),
));
_bounceAnimation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.5, 1.0, curve: Curves.elasticInOut),
));
if (widget.animated) {
_animationController.forward();
}
}
@override
void didUpdateWidget(CountBadge oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.count != oldWidget.count && widget.animated) {
_animationController.reset();
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!widget.showZero && widget.count == 0) {
return const SizedBox.shrink();
}
final displayText = _getDisplayText();
final size = widget.size ?? 20;
final backgroundColor = widget.backgroundColor ?? AppTheme.errorColor;
final textColor = widget.textColor ?? Colors.white;
Widget badge = Container(
constraints: BoxConstraints(
minWidth: size,
minHeight: size,
),
padding: EdgeInsets.symmetric(
horizontal: displayText.length > 1 ? size * 0.2 : 0,
vertical: 2,
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(size / 2),
boxShadow: [
BoxShadow(
color: backgroundColor.withOpacity(0.4),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
border: Border.all(
color: Colors.white,
width: 1.5,
),
),
child: Center(
child: Text(
displayText,
style: TextStyle(
color: textColor,
fontSize: size * 0.6,
fontWeight: FontWeight.bold,
height: 1.0,
),
textAlign: TextAlign.center,
),
),
);
if (widget.animated) {
badge = AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value * _bounceAnimation.value,
child: child,
);
},
child: badge,
);
}
if (widget.onTap != null) {
badge = GestureDetector(
onTap: widget.onTap,
child: badge,
);
}
return badge;
}
String _getDisplayText() {
if (widget.maxCount != null && widget.count > widget.maxCount!) {
return '${widget.maxCount}+';
}
final countText = widget.count.toString();
return widget.suffix != null ? '$countText${widget.suffix}' : countText;
}
}
class NotificationBadge extends StatelessWidget {
final Widget child;
final int count;
final Color? badgeColor;
final double? size;
final Offset offset;
final bool showZero;
const NotificationBadge({
super.key,
required this.child,
required this.count,
this.badgeColor,
this.size,
this.offset = const Offset(0, 0),
this.showZero = false,
});
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
child,
if (showZero || count > 0)
Positioned(
top: offset.dy,
right: offset.dx,
child: CountBadge(
count: count,
backgroundColor: badgeColor,
size: size,
showZero: showZero,
),
),
],
);
}
}

View File

@@ -1,405 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
enum BadgeType {
success,
warning,
error,
info,
neutral,
premium,
new_,
}
enum BadgeSize {
small,
medium,
large,
}
enum BadgeVariant {
filled,
outlined,
ghost,
gradient,
}
class StatusBadge extends StatelessWidget {
final String text;
final BadgeType type;
final BadgeSize size;
final BadgeVariant variant;
final IconData? icon;
final VoidCallback? onTap;
final bool animated;
final String? tooltip;
final Widget? customIcon;
final bool showPulse;
const StatusBadge({
super.key,
required this.text,
this.type = BadgeType.neutral,
this.size = BadgeSize.medium,
this.variant = BadgeVariant.filled,
this.icon,
this.onTap,
this.animated = true,
this.tooltip,
this.customIcon,
this.showPulse = false,
});
@override
Widget build(BuildContext context) {
final config = _getBadgeConfig();
Widget badge = AnimatedContainer(
duration: animated ? const Duration(milliseconds: 200) : Duration.zero,
padding: _getPadding(),
decoration: _getDecoration(config),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null || customIcon != null) ...[
_buildIcon(config),
SizedBox(width: _getIconSpacing()),
],
if (showPulse) ...[
_buildPulseIndicator(config.primaryColor),
SizedBox(width: _getIconSpacing()),
],
Text(
text,
style: _getTextStyle(config),
),
],
),
);
if (onTap != null) {
badge = Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(_getBorderRadius()),
child: badge,
),
);
}
if (tooltip != null) {
badge = Tooltip(
message: tooltip!,
child: badge,
);
}
return badge;
}
_BadgeConfig _getBadgeConfig() {
switch (type) {
case BadgeType.success:
return _BadgeConfig(
primaryColor: AppTheme.successColor,
backgroundColor: AppTheme.successColor.withOpacity(0.1),
borderColor: AppTheme.successColor.withOpacity(0.3),
);
case BadgeType.warning:
return _BadgeConfig(
primaryColor: AppTheme.warningColor,
backgroundColor: AppTheme.warningColor.withOpacity(0.1),
borderColor: AppTheme.warningColor.withOpacity(0.3),
);
case BadgeType.error:
return _BadgeConfig(
primaryColor: AppTheme.errorColor,
backgroundColor: AppTheme.errorColor.withOpacity(0.1),
borderColor: AppTheme.errorColor.withOpacity(0.3),
);
case BadgeType.info:
return _BadgeConfig(
primaryColor: AppTheme.infoColor,
backgroundColor: AppTheme.infoColor.withOpacity(0.1),
borderColor: AppTheme.infoColor.withOpacity(0.3),
);
case BadgeType.premium:
return _BadgeConfig(
primaryColor: const Color(0xFFFFD700),
backgroundColor: const Color(0xFFFFD700).withOpacity(0.1),
borderColor: const Color(0xFFFFD700).withOpacity(0.3),
);
case BadgeType.new_:
return _BadgeConfig(
primaryColor: const Color(0xFFFF6B6B),
backgroundColor: const Color(0xFFFF6B6B).withOpacity(0.1),
borderColor: const Color(0xFFFF6B6B).withOpacity(0.3),
);
default:
return _BadgeConfig(
primaryColor: AppTheme.textSecondary,
backgroundColor: AppTheme.textSecondary.withOpacity(0.1),
borderColor: AppTheme.textSecondary.withOpacity(0.3),
);
}
}
EdgeInsets _getPadding() {
switch (size) {
case BadgeSize.small:
return const EdgeInsets.symmetric(horizontal: 8, vertical: 2);
case BadgeSize.medium:
return const EdgeInsets.symmetric(horizontal: 12, vertical: 4);
case BadgeSize.large:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 8);
}
}
double _getBorderRadius() {
switch (size) {
case BadgeSize.small:
return 12;
case BadgeSize.medium:
return 16;
case BadgeSize.large:
return 20;
}
}
double _getFontSize() {
switch (size) {
case BadgeSize.small:
return 10;
case BadgeSize.medium:
return 12;
case BadgeSize.large:
return 14;
}
}
double _getIconSize() {
switch (size) {
case BadgeSize.small:
return 12;
case BadgeSize.medium:
return 14;
case BadgeSize.large:
return 16;
}
}
double _getIconSpacing() {
switch (size) {
case BadgeSize.small:
return 4;
case BadgeSize.medium:
return 6;
case BadgeSize.large:
return 8;
}
}
Decoration _getDecoration(_BadgeConfig config) {
switch (variant) {
case BadgeVariant.filled:
return BoxDecoration(
color: config.primaryColor,
borderRadius: BorderRadius.circular(_getBorderRadius()),
boxShadow: [
BoxShadow(
color: config.primaryColor.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
);
case BadgeVariant.outlined:
return BoxDecoration(
color: Colors.transparent,
border: Border.all(color: config.borderColor, width: 1),
borderRadius: BorderRadius.circular(_getBorderRadius()),
);
case BadgeVariant.ghost:
return BoxDecoration(
color: config.backgroundColor,
borderRadius: BorderRadius.circular(_getBorderRadius()),
);
case BadgeVariant.gradient:
return BoxDecoration(
gradient: LinearGradient(
colors: [
config.primaryColor,
config.primaryColor.withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(_getBorderRadius()),
boxShadow: [
BoxShadow(
color: config.primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
);
}
}
TextStyle _getTextStyle(_BadgeConfig config) {
Color textColor;
switch (variant) {
case BadgeVariant.filled:
case BadgeVariant.gradient:
textColor = Colors.white;
break;
default:
textColor = config.primaryColor;
}
return TextStyle(
fontSize: _getFontSize(),
fontWeight: FontWeight.w600,
color: textColor,
letterSpacing: 0.2,
);
}
Widget _buildIcon(_BadgeConfig config) {
Color iconColor;
switch (variant) {
case BadgeVariant.filled:
case BadgeVariant.gradient:
iconColor = Colors.white;
break;
default:
iconColor = config.primaryColor;
}
if (customIcon != null) {
return customIcon!;
}
return Icon(
icon,
size: _getIconSize(),
color: iconColor,
);
}
Widget _buildPulseIndicator(Color color) {
if (!showPulse) {
return Container(
width: _getIconSize() * 0.6,
height: _getIconSize() * 0.6,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
);
}
return _PulseWidget(
size: _getIconSize() * 0.6,
color: color,
);
}
}
class _BadgeConfig {
final Color primaryColor;
final Color backgroundColor;
final Color borderColor;
_BadgeConfig({
required this.primaryColor,
required this.backgroundColor,
required this.borderColor,
});
}
// Pulse animation widget
class _PulseWidget extends StatefulWidget {
final double size;
final Color color;
const _PulseWidget({
required this.size,
required this.color,
});
@override
State<_PulseWidget> createState() => _PulseWidgetState();
}
class _PulseWidgetState extends State<_PulseWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: 0.8 + (_animation.value * 0.4),
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.color.withOpacity(1.0 - _animation.value * 0.5),
shape: BoxShape.circle,
),
),
);
},
);
}
}
// Extension for easy badge creation
extension BadgeBuilder on String {
StatusBadge toBadge({
BadgeType type = BadgeType.neutral,
BadgeSize size = BadgeSize.medium,
BadgeVariant variant = BadgeVariant.filled,
IconData? icon,
VoidCallback? onTap,
}) {
return StatusBadge(
text: this,
type: type,
size: size,
variant: variant,
icon: icon,
onTap: onTap,
);
}
}

View File

@@ -1,383 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../theme/app_theme.dart';
enum ButtonGroupVariant {
segmented,
toggle,
tabs,
chips,
}
class ButtonGroupOption {
final String text;
final IconData? icon;
final String value;
final bool disabled;
final Widget? badge;
const ButtonGroupOption({
required this.text,
required this.value,
this.icon,
this.disabled = false,
this.badge,
});
}
class SophisticatedButtonGroup extends StatefulWidget {
final List<ButtonGroupOption> options;
final String? selectedValue;
final List<String>? selectedValues; // For multi-select
final Function(String)? onSelectionChanged;
final Function(List<String>)? onMultiSelectionChanged;
final ButtonGroupVariant variant;
final bool multiSelect;
final Color? backgroundColor;
final Color? selectedColor;
final Color? unselectedColor;
final double? height;
final EdgeInsets? padding;
final bool animated;
final bool fullWidth;
const SophisticatedButtonGroup({
super.key,
required this.options,
this.selectedValue,
this.selectedValues,
this.onSelectionChanged,
this.onMultiSelectionChanged,
this.variant = ButtonGroupVariant.segmented,
this.multiSelect = false,
this.backgroundColor,
this.selectedColor,
this.unselectedColor,
this.height,
this.padding,
this.animated = true,
this.fullWidth = false,
});
@override
State<SophisticatedButtonGroup> createState() => _SophisticatedButtonGroupState();
}
class _SophisticatedButtonGroupState extends State<SophisticatedButtonGroup>
with TickerProviderStateMixin {
late AnimationController _slideController;
late Animation<double> _slideAnimation;
String? _internalSelectedValue;
List<String> _internalSelectedValues = [];
@override
void initState() {
super.initState();
_slideController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_slideAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeInOut,
));
_internalSelectedValue = widget.selectedValue;
_internalSelectedValues = widget.selectedValues ?? [];
if (widget.animated) {
_slideController.forward();
}
}
@override
void didUpdateWidget(SophisticatedButtonGroup oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedValue != oldWidget.selectedValue) {
_internalSelectedValue = widget.selectedValue;
}
if (widget.selectedValues != oldWidget.selectedValues) {
_internalSelectedValues = widget.selectedValues ?? [];
}
}
@override
void dispose() {
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
switch (widget.variant) {
case ButtonGroupVariant.segmented:
return _buildSegmentedGroup();
case ButtonGroupVariant.toggle:
return _buildToggleGroup();
case ButtonGroupVariant.tabs:
return _buildTabsGroup();
case ButtonGroupVariant.chips:
return _buildChipsGroup();
}
}
Widget _buildSegmentedGroup() {
return AnimatedBuilder(
animation: _slideAnimation,
builder: (context, child) {
return Container(
height: widget.height ?? 48,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: widget.backgroundColor ?? AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.textHint.withOpacity(0.2),
width: 1,
),
),
child: Row(
children: widget.options.asMap().entries.map((entry) {
final index = entry.key;
final option = entry.value;
final isSelected = _isSelected(option.value);
return Expanded(
child: _buildSegmentedButton(option, isSelected, index),
);
}).toList(),
),
);
},
);
}
Widget _buildSegmentedButton(ButtonGroupOption option, bool isSelected, int index) {
return AnimatedContainer(
duration: widget.animated ? const Duration(milliseconds: 200) : Duration.zero,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: isSelected
? (widget.selectedColor ?? AppTheme.primaryColor)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isSelected ? [
BoxShadow(
color: (widget.selectedColor ?? AppTheme.primaryColor).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
] : null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: option.disabled ? null : () => _handleSelection(option.value),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: _buildButtonContent(option, isSelected),
),
),
),
);
}
Widget _buildToggleGroup() {
return Wrap(
spacing: 8,
runSpacing: 8,
children: widget.options.map((option) {
final isSelected = _isSelected(option.value);
return _buildToggleButton(option, isSelected);
}).toList(),
);
}
Widget _buildToggleButton(ButtonGroupOption option, bool isSelected) {
return AnimatedContainer(
duration: widget.animated ? const Duration(milliseconds: 200) : Duration.zero,
decoration: BoxDecoration(
color: isSelected
? (widget.selectedColor ?? AppTheme.primaryColor)
: (widget.backgroundColor ?? Colors.transparent),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isSelected
? (widget.selectedColor ?? AppTheme.primaryColor)
: AppTheme.textHint.withOpacity(0.3),
width: 1.5,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: option.disabled ? null : () => _handleSelection(option.value),
borderRadius: BorderRadius.circular(24),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: _buildButtonContent(option, isSelected),
),
),
),
);
}
Widget _buildTabsGroup() {
return Container(
height: widget.height ?? 44,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppTheme.textHint.withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: widget.options.asMap().entries.map((entry) {
final index = entry.key;
final option = entry.value;
final isSelected = _isSelected(option.value);
return widget.fullWidth
? Expanded(child: _buildTabButton(option, isSelected))
: _buildTabButton(option, isSelected);
}).toList(),
),
);
}
Widget _buildTabButton(ButtonGroupOption option, bool isSelected) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: option.disabled ? null : () => _handleSelection(option.value),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected
? (widget.selectedColor ?? AppTheme.primaryColor)
: Colors.transparent,
width: 2,
),
),
),
child: _buildButtonContent(option, isSelected),
),
),
),
);
}
Widget _buildChipsGroup() {
return Wrap(
spacing: 8,
runSpacing: 8,
children: widget.options.map((option) {
final isSelected = _isSelected(option.value);
return _buildChip(option, isSelected);
}).toList(),
);
}
Widget _buildChip(ButtonGroupOption option, bool isSelected) {
return FilterChip(
label: _buildButtonContent(option, isSelected),
selected: isSelected,
onSelected: option.disabled ? null : (selected) => _handleSelection(option.value),
backgroundColor: widget.backgroundColor,
selectedColor: widget.selectedColor ?? AppTheme.primaryColor,
checkmarkColor: Colors.white,
labelStyle: TextStyle(
color: isSelected ? Colors.white : (widget.unselectedColor ?? AppTheme.textPrimary),
fontWeight: FontWeight.w600,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
);
}
Widget _buildButtonContent(ButtonGroupOption option, bool isSelected) {
final color = isSelected
? Colors.white
: (widget.unselectedColor ?? AppTheme.textSecondary);
if (option.icon != null) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
option.icon,
size: 16,
color: color,
),
const SizedBox(width: 6),
Text(
option.text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
if (option.badge != null) ...[
const SizedBox(width: 6),
option.badge!,
],
],
);
}
return Text(
option.text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 14,
),
textAlign: TextAlign.center,
);
}
bool _isSelected(String value) {
if (widget.multiSelect) {
return _internalSelectedValues.contains(value);
}
return _internalSelectedValue == value;
}
void _handleSelection(String value) {
HapticFeedback.selectionClick();
if (widget.multiSelect) {
setState(() {
if (_internalSelectedValues.contains(value)) {
_internalSelectedValues.remove(value);
} else {
_internalSelectedValues.add(value);
}
});
widget.onMultiSelectionChanged?.call(_internalSelectedValues);
} else {
setState(() {
_internalSelectedValue = value;
});
widget.onSelectionChanged?.call(value);
}
}
}

View File

@@ -1,303 +0,0 @@
// Export all sophisticated button components
export 'sophisticated_button.dart';
export 'floating_action_button.dart';
export 'icon_button.dart';
export 'button_group.dart';
// Predefined button styles for quick usage
import 'package:flutter/material.dart';
import 'sophisticated_button.dart';
import 'floating_action_button.dart';
import 'icon_button.dart';
import '../../theme/app_theme.dart';
// Quick button factory methods
class QuickButtons {
// Primary buttons
static Widget primary({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
bool loading = false,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.primary,
size: size,
loading: loading,
);
}
static Widget secondary({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
bool loading = false,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.secondary,
size: size,
loading: loading,
);
}
static Widget outline({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
Color? color,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.outline,
size: size,
backgroundColor: color,
);
}
static Widget ghost({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
Color? color,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.ghost,
size: size,
backgroundColor: color,
);
}
static Widget gradient({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
Gradient? gradient,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.gradient,
size: size,
gradient: gradient,
);
}
static Widget glass({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.glass,
size: size,
);
}
static Widget danger({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.danger,
size: size,
);
}
static Widget success({
required String text,
required VoidCallback onPressed,
IconData? icon,
ButtonSize size = ButtonSize.medium,
}) {
return SophisticatedButton(
text: text,
icon: icon,
onPressed: onPressed,
variant: ButtonVariant.success,
size: size,
);
}
// Icon buttons
static Widget iconPrimary({
required IconData icon,
required VoidCallback onPressed,
double? size,
String? tooltip,
int? notificationCount,
}) {
return SophisticatedIconButton(
icon: icon,
onPressed: onPressed,
variant: IconButtonVariant.filled,
backgroundColor: AppTheme.primaryColor,
size: size,
tooltip: tooltip,
notificationCount: notificationCount,
);
}
static Widget iconSecondary({
required IconData icon,
required VoidCallback onPressed,
double? size,
String? tooltip,
int? notificationCount,
}) {
return SophisticatedIconButton(
icon: icon,
onPressed: onPressed,
variant: IconButtonVariant.filled,
backgroundColor: AppTheme.secondaryColor,
size: size,
tooltip: tooltip,
notificationCount: notificationCount,
);
}
static Widget iconOutline({
required IconData icon,
required VoidCallback onPressed,
double? size,
String? tooltip,
Color? color,
}) {
return SophisticatedIconButton(
icon: icon,
onPressed: onPressed,
variant: IconButtonVariant.outlined,
foregroundColor: color ?? AppTheme.primaryColor,
borderColor: color ?? AppTheme.primaryColor,
size: size,
tooltip: tooltip,
);
}
static Widget iconGhost({
required IconData icon,
required VoidCallback onPressed,
double? size,
String? tooltip,
Color? color,
}) {
return SophisticatedIconButton(
icon: icon,
onPressed: onPressed,
variant: IconButtonVariant.ghost,
backgroundColor: color ?? AppTheme.primaryColor,
size: size,
tooltip: tooltip,
);
}
static Widget iconGradient({
required IconData icon,
required VoidCallback onPressed,
double? size,
String? tooltip,
Gradient? gradient,
}) {
return SophisticatedIconButton(
icon: icon,
onPressed: onPressed,
variant: IconButtonVariant.gradient,
gradient: gradient,
size: size,
tooltip: tooltip,
);
}
// FAB buttons
static Widget fab({
required VoidCallback onPressed,
IconData icon = Icons.add,
FABVariant variant = FABVariant.primary,
FABSize size = FABSize.regular,
String? tooltip,
}) {
return SophisticatedFAB(
icon: icon,
onPressed: onPressed,
variant: variant,
size: size,
tooltip: tooltip,
);
}
static Widget fabExtended({
required String label,
required VoidCallback onPressed,
IconData icon = Icons.add,
FABVariant variant = FABVariant.primary,
String? tooltip,
}) {
return SophisticatedFAB(
icon: icon,
label: label,
onPressed: onPressed,
variant: variant,
size: FABSize.extended,
tooltip: tooltip,
);
}
static Widget fabGradient({
required VoidCallback onPressed,
IconData icon = Icons.add,
FABSize size = FABSize.regular,
Gradient? gradient,
String? tooltip,
}) {
return SophisticatedFAB(
icon: icon,
onPressed: onPressed,
variant: FABVariant.gradient,
size: size,
gradient: gradient,
tooltip: tooltip,
);
}
static Widget fabMorphing({
required VoidCallback onPressed,
required List<IconData> icons,
FABSize size = FABSize.regular,
Duration morphingDuration = const Duration(seconds: 2),
String? tooltip,
}) {
return SophisticatedFAB(
onPressed: onPressed,
variant: FABVariant.morphing,
size: size,
morphingIcons: icons,
morphingDuration: morphingDuration,
tooltip: tooltip,
);
}
}

View File

@@ -1,400 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../theme/app_theme.dart';
enum FABVariant {
primary,
secondary,
gradient,
glass,
morphing,
}
enum FABSize {
small,
regular,
large,
extended,
}
class SophisticatedFAB extends StatefulWidget {
final IconData? icon;
final String? label;
final VoidCallback? onPressed;
final FABVariant variant;
final FABSize size;
final Color? backgroundColor;
final Color? foregroundColor;
final Gradient? gradient;
final bool animated;
final bool showPulse;
final List<IconData>? morphingIcons;
final Duration morphingDuration;
final String? tooltip;
const SophisticatedFAB({
super.key,
this.icon,
this.label,
this.onPressed,
this.variant = FABVariant.primary,
this.size = FABSize.regular,
this.backgroundColor,
this.foregroundColor,
this.gradient,
this.animated = true,
this.showPulse = false,
this.morphingIcons,
this.morphingDuration = const Duration(seconds: 2),
this.tooltip,
});
@override
State<SophisticatedFAB> createState() => _SophisticatedFABState();
}
class _SophisticatedFABState extends State<SophisticatedFAB>
with TickerProviderStateMixin {
late AnimationController _scaleController;
late AnimationController _rotationController;
late AnimationController _pulseController;
late AnimationController _morphingController;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<double> _pulseAnimation;
int _currentMorphingIndex = 0;
@override
void initState() {
super.initState();
_scaleController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_rotationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_morphingController = AnimationController(
duration: widget.morphingDuration,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.9,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.elasticOut,
));
_pulseAnimation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
if (widget.showPulse) {
_pulseController.repeat(reverse: true);
}
if (widget.morphingIcons != null && widget.morphingIcons!.isNotEmpty) {
_startMorphing();
}
}
void _startMorphing() {
_morphingController.addListener(() {
if (_morphingController.isCompleted) {
setState(() {
_currentMorphingIndex =
(_currentMorphingIndex + 1) % widget.morphingIcons!.length;
});
_morphingController.reset();
_morphingController.forward();
}
});
_morphingController.forward();
}
@override
void dispose() {
_scaleController.dispose();
_rotationController.dispose();
_pulseController.dispose();
_morphingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final config = _getFABConfig();
Widget fab = AnimatedBuilder(
animation: Listenable.merge([
_scaleController,
_rotationController,
_pulseController,
]),
builder: (context, child) {
return Transform.scale(
scale: widget.animated
? _scaleAnimation.value * (widget.showPulse ? _pulseAnimation.value : 1.0)
: 1.0,
child: Transform.rotate(
angle: widget.animated ? _rotationAnimation.value * 0.1 : 0.0,
child: Container(
width: _getSize(),
height: _getSize(),
decoration: _getDecoration(config),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _handleTap,
onTapDown: widget.animated ? (_) => _scaleController.forward() : null,
onTapUp: widget.animated ? (_) => _scaleController.reverse() : null,
onTapCancel: widget.animated ? () => _scaleController.reverse() : null,
customBorder: const CircleBorder(),
child: _buildContent(config),
),
),
),
),
);
},
);
if (widget.tooltip != null) {
fab = Tooltip(
message: widget.tooltip!,
child: fab,
);
}
return fab;
}
Widget _buildContent(_FABConfig config) {
if (widget.size == FABSize.extended && widget.label != null) {
return _buildExtendedContent(config);
}
return Center(
child: _buildIcon(config),
);
}
Widget _buildExtendedContent(_FABConfig config) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildIcon(config),
const SizedBox(width: 8),
Text(
widget.label!,
style: TextStyle(
color: config.foregroundColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildIcon(_FABConfig config) {
IconData iconToShow = widget.icon ?? Icons.add;
if (widget.morphingIcons != null && widget.morphingIcons!.isNotEmpty) {
iconToShow = widget.morphingIcons![_currentMorphingIndex];
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: RotationTransition(
turns: animation,
child: child,
),
);
},
child: Icon(
iconToShow,
key: ValueKey(iconToShow),
color: config.foregroundColor,
size: _getIconSize(),
),
);
}
_FABConfig _getFABConfig() {
switch (widget.variant) {
case FABVariant.primary:
return _FABConfig(
backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
);
case FABVariant.secondary:
return _FABConfig(
backgroundColor: widget.backgroundColor ?? AppTheme.secondaryColor,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
);
case FABVariant.gradient:
return _FABConfig(
backgroundColor: Colors.transparent,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
useGradient: true,
);
case FABVariant.glass:
return _FABConfig(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
borderColor: Colors.white.withOpacity(0.3),
hasElevation: true,
isGlass: true,
);
case FABVariant.morphing:
return _FABConfig(
backgroundColor: widget.backgroundColor ?? AppTheme.accentColor,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
isMorphing: true,
);
}
}
Decoration _getDecoration(_FABConfig config) {
if (config.useGradient) {
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [
widget.backgroundColor ?? AppTheme.primaryColor,
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: config.hasElevation ? _getShadow(config) : null,
);
}
return BoxDecoration(
color: config.backgroundColor,
shape: BoxShape.circle,
border: config.borderColor != null
? Border.all(color: config.borderColor!, width: 1)
: null,
boxShadow: config.hasElevation ? _getShadow(config) : null,
);
}
List<BoxShadow> _getShadow(_FABConfig config) {
final shadowColor = config.useGradient
? (widget.backgroundColor ?? AppTheme.primaryColor)
: config.backgroundColor;
return [
BoxShadow(
color: shadowColor.withOpacity(0.4),
blurRadius: 20,
offset: const Offset(0, 8),
),
BoxShadow(
color: shadowColor.withOpacity(0.2),
blurRadius: 40,
offset: const Offset(0, 16),
),
];
}
double _getSize() {
switch (widget.size) {
case FABSize.small:
return 40;
case FABSize.regular:
return 56;
case FABSize.large:
return 72;
case FABSize.extended:
return 56; // Height for extended FAB
}
}
double _getIconSize() {
switch (widget.size) {
case FABSize.small:
return 20;
case FABSize.regular:
return 24;
case FABSize.large:
return 32;
case FABSize.extended:
return 24;
}
}
void _handleTap() {
HapticFeedback.lightImpact();
if (widget.animated) {
_rotationController.forward().then((_) {
_rotationController.reverse();
});
}
widget.onPressed?.call();
}
}
class _FABConfig {
final Color backgroundColor;
final Color foregroundColor;
final Color? borderColor;
final bool hasElevation;
final bool useGradient;
final bool isGlass;
final bool isMorphing;
_FABConfig({
required this.backgroundColor,
required this.foregroundColor,
this.borderColor,
this.hasElevation = false,
this.useGradient = false,
this.isGlass = false,
this.isMorphing = false,
});
}

View File

@@ -1,356 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../theme/app_theme.dart';
import '../badges/count_badge.dart';
enum IconButtonVariant {
standard,
filled,
outlined,
ghost,
gradient,
glass,
}
enum IconButtonShape {
circle,
rounded,
square,
}
class SophisticatedIconButton extends StatefulWidget {
final IconData icon;
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
final IconButtonVariant variant;
final IconButtonShape shape;
final double? size;
final Color? backgroundColor;
final Color? foregroundColor;
final Color? borderColor;
final Gradient? gradient;
final bool animated;
final bool disabled;
final String? tooltip;
final Widget? badge;
final int? notificationCount;
final bool showPulse;
const SophisticatedIconButton({
super.key,
required this.icon,
this.onPressed,
this.onLongPress,
this.variant = IconButtonVariant.standard,
this.shape = IconButtonShape.circle,
this.size,
this.backgroundColor,
this.foregroundColor,
this.borderColor,
this.gradient,
this.animated = true,
this.disabled = false,
this.tooltip,
this.badge,
this.notificationCount,
this.showPulse = false,
});
@override
State<SophisticatedIconButton> createState() => _SophisticatedIconButtonState();
}
class _SophisticatedIconButtonState extends State<SophisticatedIconButton>
with TickerProviderStateMixin {
late AnimationController _pressController;
late AnimationController _pulseController;
late AnimationController _rotationController;
late Animation<double> _scaleAnimation;
late Animation<double> _pulseAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_pressController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_pulseController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_rotationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.9,
).animate(CurvedAnimation(
parent: _pressController,
curve: Curves.easeInOut,
));
_pulseAnimation = Tween<double>(
begin: 1.0,
end: 1.1,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.25,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.elasticOut,
));
if (widget.showPulse) {
_pulseController.repeat(reverse: true);
}
}
@override
void dispose() {
_pressController.dispose();
_pulseController.dispose();
_rotationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final config = _getButtonConfig();
final buttonSize = widget.size ?? 48.0;
final iconSize = buttonSize * 0.5;
Widget button = AnimatedBuilder(
animation: Listenable.merge([_pressController, _pulseController, _rotationController]),
builder: (context, child) {
return Transform.scale(
scale: widget.animated
? _scaleAnimation.value * (widget.showPulse ? _pulseAnimation.value : 1.0)
: 1.0,
child: Transform.rotate(
angle: widget.animated ? _rotationAnimation.value : 0.0,
child: Container(
width: buttonSize,
height: buttonSize,
decoration: _getDecoration(config, buttonSize),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.disabled ? null : _handleTap,
onLongPress: widget.disabled ? null : widget.onLongPress,
onTapDown: widget.animated && !widget.disabled ? (_) => _pressController.forward() : null,
onTapUp: widget.animated && !widget.disabled ? (_) => _pressController.reverse() : null,
onTapCancel: widget.animated && !widget.disabled ? () => _pressController.reverse() : null,
customBorder: _getInkWellBorder(buttonSize),
child: Center(
child: Icon(
widget.icon,
size: iconSize,
color: widget.disabled
? AppTheme.textHint
: config.foregroundColor,
),
),
),
),
),
),
);
},
);
// Add badge if provided
if (widget.badge != null || widget.notificationCount != null) {
button = Stack(
clipBehavior: Clip.none,
children: [
button,
if (widget.notificationCount != null)
Positioned(
top: -8,
right: -8,
child: CountBadge(
count: widget.notificationCount!,
size: 18,
),
),
if (widget.badge != null)
Positioned(
top: -4,
right: -4,
child: widget.badge!,
),
],
);
}
if (widget.tooltip != null) {
button = Tooltip(
message: widget.tooltip!,
child: button,
);
}
return button;
}
_IconButtonConfig _getButtonConfig() {
switch (widget.variant) {
case IconButtonVariant.standard:
return _IconButtonConfig(
backgroundColor: Colors.transparent,
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
hasElevation: false,
);
case IconButtonVariant.filled:
return _IconButtonConfig(
backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
);
case IconButtonVariant.outlined:
return _IconButtonConfig(
backgroundColor: Colors.transparent,
foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor,
borderColor: widget.borderColor ?? AppTheme.primaryColor,
hasElevation: false,
);
case IconButtonVariant.ghost:
return _IconButtonConfig(
backgroundColor: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.1),
foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor,
hasElevation: false,
);
case IconButtonVariant.gradient:
return _IconButtonConfig(
backgroundColor: Colors.transparent,
foregroundColor: widget.foregroundColor ?? Colors.white,
hasElevation: true,
useGradient: true,
);
case IconButtonVariant.glass:
return _IconButtonConfig(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
borderColor: Colors.white.withOpacity(0.3),
hasElevation: true,
isGlass: true,
);
}
}
Decoration _getDecoration(_IconButtonConfig config, double size) {
final borderRadius = _getBorderRadius(size);
if (config.useGradient) {
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [
widget.backgroundColor ?? AppTheme.primaryColor,
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: borderRadius,
boxShadow: config.hasElevation ? _getShadow(config, size) : null,
);
}
return BoxDecoration(
color: config.backgroundColor,
borderRadius: borderRadius,
border: config.borderColor != null
? Border.all(color: config.borderColor!, width: 1.5)
: null,
boxShadow: config.hasElevation && !widget.disabled ? _getShadow(config, size) : null,
);
}
BorderRadius _getBorderRadius(double size) {
switch (widget.shape) {
case IconButtonShape.circle:
return BorderRadius.circular(size / 2);
case IconButtonShape.rounded:
return BorderRadius.circular(size * 0.25);
case IconButtonShape.square:
return BorderRadius.circular(8);
}
}
ShapeBorder _getInkWellBorder(double size) {
switch (widget.shape) {
case IconButtonShape.circle:
return const CircleBorder();
case IconButtonShape.rounded:
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(size * 0.25),
);
case IconButtonShape.square:
return RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
}
}
List<BoxShadow> _getShadow(_IconButtonConfig config, double size) {
final shadowColor = config.useGradient
? (widget.backgroundColor ?? AppTheme.primaryColor)
: config.backgroundColor;
return [
BoxShadow(
color: shadowColor.withOpacity(0.3),
blurRadius: size * 0.3,
offset: Offset(0, size * 0.1),
),
];
}
void _handleTap() {
HapticFeedback.selectionClick();
if (widget.animated) {
_rotationController.forward().then((_) {
_rotationController.reverse();
});
}
widget.onPressed?.call();
}
}
class _IconButtonConfig {
final Color backgroundColor;
final Color foregroundColor;
final Color? borderColor;
final bool hasElevation;
final bool useGradient;
final bool isGlass;
_IconButtonConfig({
required this.backgroundColor,
required this.foregroundColor,
this.borderColor,
this.hasElevation = false,
this.useGradient = false,
this.isGlass = false,
});
}

View File

@@ -1,291 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
/// Widget bouton principal réutilisable
class PrimaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isEnabled;
final IconData? icon;
final Color? backgroundColor;
final Color? textColor;
final double? width;
final double height;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
const PrimaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isEnabled = true,
this.icon,
this.backgroundColor,
this.textColor,
this.width,
this.height = 48.0,
this.padding,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final effectiveBackgroundColor = backgroundColor ?? AppTheme.primaryColor;
final effectiveTextColor = textColor ?? Colors.white;
final isButtonEnabled = isEnabled && !isLoading && onPressed != null;
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: isButtonEnabled ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: effectiveBackgroundColor,
foregroundColor: effectiveTextColor,
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5),
disabledForegroundColor: effectiveTextColor.withOpacity(0.5),
elevation: isButtonEnabled ? 2 : 0,
shadowColor: effectiveBackgroundColor.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(8),
),
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveTextColor),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: effectiveTextColor,
),
),
],
),
),
);
}
}
/// Widget bouton secondaire
class SecondaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isEnabled;
final IconData? icon;
final Color? borderColor;
final Color? textColor;
final double? width;
final double height;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
const SecondaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isEnabled = true,
this.icon,
this.borderColor,
this.textColor,
this.width,
this.height = 48.0,
this.padding,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
final effectiveBorderColor = borderColor ?? AppTheme.primaryColor;
final effectiveTextColor = textColor ?? AppTheme.primaryColor;
final isButtonEnabled = isEnabled && !isLoading && onPressed != null;
return SizedBox(
width: width,
height: height,
child: OutlinedButton(
onPressed: isButtonEnabled ? onPressed : null,
style: OutlinedButton.styleFrom(
foregroundColor: effectiveTextColor,
disabledForegroundColor: effectiveTextColor.withOpacity(0.5),
side: BorderSide(
color: isButtonEnabled ? effectiveBorderColor : effectiveBorderColor.withOpacity(0.5),
width: 1.5,
),
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(8),
),
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveTextColor),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: effectiveTextColor,
),
),
],
),
),
);
}
}
/// Widget bouton texte
class CustomTextButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isEnabled;
final IconData? icon;
final Color? textColor;
final double? width;
final double height;
final EdgeInsetsGeometry? padding;
const CustomTextButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isEnabled = true,
this.icon,
this.textColor,
this.width,
this.height = 48.0,
this.padding,
});
@override
Widget build(BuildContext context) {
final effectiveTextColor = textColor ?? AppTheme.primaryColor;
final isButtonEnabled = isEnabled && !isLoading && onPressed != null;
return SizedBox(
width: width,
height: height,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isButtonEnabled ? onPressed : null,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(effectiveTextColor),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(
icon,
size: 18,
color: isButtonEnabled ? effectiveTextColor : effectiveTextColor.withOpacity(0.5),
),
const SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isButtonEnabled ? effectiveTextColor : effectiveTextColor.withOpacity(0.5),
),
),
],
),
),
),
),
);
}
}
/// Widget bouton destructeur (pour les actions dangereuses)
class DestructiveButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isEnabled;
final IconData? icon;
final double? width;
final double height;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
const DestructiveButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isEnabled = true,
this.icon,
this.width,
this.height = 48.0,
this.padding,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return PrimaryButton(
text: text,
onPressed: onPressed,
isLoading: isLoading,
isEnabled: isEnabled,
icon: icon,
backgroundColor: AppTheme.errorColor,
textColor: Colors.white,
width: width,
height: height,
padding: padding,
borderRadius: borderRadius,
);
}
}

View File

@@ -1,554 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../theme/app_theme.dart';
enum ButtonVariant {
primary,
secondary,
outline,
ghost,
gradient,
glass,
danger,
success,
}
enum ButtonSize {
small,
medium,
large,
extraLarge,
}
enum ButtonShape {
rounded,
circular,
square,
}
class SophisticatedButton extends StatefulWidget {
final String? text;
final Widget? child;
final IconData? icon;
final IconData? suffixIcon;
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
final ButtonVariant variant;
final ButtonSize size;
final ButtonShape shape;
final Color? backgroundColor;
final Color? foregroundColor;
final Gradient? gradient;
final bool loading;
final bool disabled;
final bool animated;
final bool showRipple;
final double? width;
final double? height;
final EdgeInsets? padding;
final List<BoxShadow>? customShadow;
final String? tooltip;
final bool hapticFeedback;
const SophisticatedButton({
super.key,
this.text,
this.child,
this.icon,
this.suffixIcon,
this.onPressed,
this.onLongPress,
this.variant = ButtonVariant.primary,
this.size = ButtonSize.medium,
this.shape = ButtonShape.rounded,
this.backgroundColor,
this.foregroundColor,
this.gradient,
this.loading = false,
this.disabled = false,
this.animated = true,
this.showRipple = true,
this.width,
this.height,
this.padding,
this.customShadow,
this.tooltip,
this.hapticFeedback = true,
});
@override
State<SophisticatedButton> createState() => _SophisticatedButtonState();
}
class _SophisticatedButtonState extends State<SophisticatedButton>
with TickerProviderStateMixin {
late AnimationController _pressController;
late AnimationController _loadingController;
late AnimationController _shimmerController;
late Animation<double> _scaleAnimation;
late Animation<double> _shadowAnimation;
late Animation<double> _loadingAnimation;
late Animation<double> _shimmerAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_pressController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_loadingController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_shimmerController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _pressController,
curve: Curves.easeInOut,
));
_shadowAnimation = Tween<double>(
begin: 1.0,
end: 0.7,
).animate(CurvedAnimation(
parent: _pressController,
curve: Curves.easeInOut,
));
_loadingAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _loadingController,
curve: Curves.easeInOut,
));
_shimmerAnimation = Tween<double>(
begin: -1.0,
end: 2.0,
).animate(CurvedAnimation(
parent: _shimmerController,
curve: Curves.easeInOut,
));
if (widget.loading) {
_loadingController.repeat();
}
// Shimmer effect for premium buttons
if (widget.variant == ButtonVariant.gradient) {
_shimmerController.repeat();
}
}
@override
void didUpdateWidget(SophisticatedButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.loading != oldWidget.loading) {
if (widget.loading) {
_loadingController.repeat();
} else {
_loadingController.reset();
}
}
}
@override
void dispose() {
_pressController.dispose();
_loadingController.dispose();
_shimmerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final config = _getButtonConfig();
final isDisabled = widget.disabled || widget.loading;
Widget button = AnimatedBuilder(
animation: Listenable.merge([_pressController, _loadingController, _shimmerController]),
builder: (context, child) {
return Transform.scale(
scale: widget.animated ? _scaleAnimation.value : 1.0,
child: Container(
width: widget.width,
height: widget.height ?? _getHeight(),
padding: widget.padding ?? _getPadding(),
decoration: _getDecoration(config),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isDisabled ? null : _handleTap,
onLongPress: isDisabled ? null : widget.onLongPress,
onTapDown: widget.animated && !isDisabled ? (_) => _pressController.forward() : null,
onTapUp: widget.animated && !isDisabled ? (_) => _pressController.reverse() : null,
onTapCancel: widget.animated && !isDisabled ? () => _pressController.reverse() : null,
borderRadius: _getBorderRadius(),
splashColor: widget.showRipple ? config.foregroundColor.withOpacity(0.2) : Colors.transparent,
highlightColor: widget.showRipple ? config.foregroundColor.withOpacity(0.1) : Colors.transparent,
child: _buildContent(config),
),
),
),
);
},
);
if (widget.tooltip != null) {
button = Tooltip(
message: widget.tooltip!,
child: button,
);
}
return button;
}
Widget _buildContent(_ButtonConfig config) {
final hasIcon = widget.icon != null;
final hasSuffixIcon = widget.suffixIcon != null;
final hasText = widget.text != null || widget.child != null;
if (widget.loading) {
return _buildLoadingContent(config);
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (hasIcon) ...[
_buildIcon(widget.icon!, config),
if (hasText) SizedBox(width: _getIconSpacing()),
],
if (hasText) ...[
Flexible(child: _buildText(config)),
],
if (hasSuffixIcon) ...[
if (hasText || hasIcon) SizedBox(width: _getIconSpacing()),
_buildIcon(widget.suffixIcon!, config),
],
],
);
}
Widget _buildLoadingContent(_ButtonConfig config) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: _getIconSize(),
height: _getIconSize(),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(config.foregroundColor),
),
),
if (widget.text != null) ...[
SizedBox(width: _getIconSpacing()),
Text(
'Chargement...',
style: _getTextStyle(config),
),
],
],
);
}
Widget _buildIcon(IconData icon, _ButtonConfig config) {
return Icon(
icon,
size: _getIconSize(),
color: config.foregroundColor,
);
}
Widget _buildText(_ButtonConfig config) {
if (widget.child != null) {
return DefaultTextStyle(
style: _getTextStyle(config),
child: widget.child!,
);
}
return Text(
widget.text!,
style: _getTextStyle(config),
textAlign: TextAlign.center,
);
}
_ButtonConfig _getButtonConfig() {
final isDisabled = widget.disabled || widget.loading;
switch (widget.variant) {
case ButtonVariant.primary:
return _ButtonConfig(
backgroundColor: isDisabled
? AppTheme.textHint
: (widget.backgroundColor ?? AppTheme.primaryColor),
foregroundColor: isDisabled
? AppTheme.textSecondary
: (widget.foregroundColor ?? Colors.white),
hasElevation: true,
);
case ButtonVariant.secondary:
return _ButtonConfig(
backgroundColor: isDisabled
? AppTheme.backgroundLight
: (widget.backgroundColor ?? AppTheme.secondaryColor),
foregroundColor: isDisabled
? AppTheme.textHint
: (widget.foregroundColor ?? Colors.white),
hasElevation: true,
);
case ButtonVariant.outline:
return _ButtonConfig(
backgroundColor: Colors.transparent,
foregroundColor: isDisabled
? AppTheme.textHint
: (widget.foregroundColor ?? AppTheme.primaryColor),
borderColor: isDisabled
? AppTheme.textHint
: (widget.backgroundColor ?? AppTheme.primaryColor),
hasElevation: false,
);
case ButtonVariant.ghost:
return _ButtonConfig(
backgroundColor: isDisabled
? Colors.transparent
: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.1),
foregroundColor: isDisabled
? AppTheme.textHint
: (widget.foregroundColor ?? AppTheme.primaryColor),
hasElevation: false,
);
case ButtonVariant.gradient:
return _ButtonConfig(
backgroundColor: Colors.transparent,
foregroundColor: isDisabled
? AppTheme.textHint
: (widget.foregroundColor ?? Colors.white),
hasElevation: true,
useGradient: true,
);
case ButtonVariant.glass:
return _ButtonConfig(
backgroundColor: isDisabled
? Colors.grey.withOpacity(0.1)
: Colors.white.withOpacity(0.2),
foregroundColor: isDisabled
? AppTheme.textHint
: (widget.foregroundColor ?? AppTheme.textPrimary),
borderColor: Colors.white.withOpacity(0.3),
hasElevation: true,
isGlass: true,
);
case ButtonVariant.danger:
return _ButtonConfig(
backgroundColor: isDisabled
? AppTheme.textHint
: AppTheme.errorColor,
foregroundColor: isDisabled
? AppTheme.textSecondary
: Colors.white,
hasElevation: true,
);
case ButtonVariant.success:
return _ButtonConfig(
backgroundColor: isDisabled
? AppTheme.textHint
: AppTheme.successColor,
foregroundColor: isDisabled
? AppTheme.textSecondary
: Colors.white,
hasElevation: true,
);
}
}
Decoration _getDecoration(_ButtonConfig config) {
final borderRadius = _getBorderRadius();
final isDisabled = widget.disabled || widget.loading;
if (config.useGradient && !isDisabled) {
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [
widget.backgroundColor ?? AppTheme.primaryColor,
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: borderRadius,
boxShadow: config.hasElevation ? _getShadow(config) : null,
);
}
return BoxDecoration(
color: config.backgroundColor,
borderRadius: borderRadius,
border: config.borderColor != null
? Border.all(color: config.borderColor!, width: 1.5)
: null,
boxShadow: config.hasElevation && !isDisabled ? _getShadow(config) : null,
);
}
List<BoxShadow> _getShadow(_ButtonConfig config) {
if (widget.customShadow != null) {
return widget.customShadow!.map((shadow) => BoxShadow(
color: shadow.color.withOpacity(shadow.color.opacity * _shadowAnimation.value),
blurRadius: shadow.blurRadius * _shadowAnimation.value,
offset: shadow.offset * _shadowAnimation.value,
spreadRadius: shadow.spreadRadius,
)).toList();
}
final shadowColor = config.useGradient
? (widget.backgroundColor ?? AppTheme.primaryColor)
: config.backgroundColor;
return [
BoxShadow(
color: shadowColor.withOpacity(0.3 * _shadowAnimation.value),
blurRadius: 15 * _shadowAnimation.value,
offset: Offset(0, 8 * _shadowAnimation.value),
),
];
}
BorderRadius _getBorderRadius() {
switch (widget.shape) {
case ButtonShape.rounded:
return BorderRadius.circular(_getHeight() / 2);
case ButtonShape.circular:
return BorderRadius.circular(_getHeight());
case ButtonShape.square:
return BorderRadius.circular(8);
}
}
double _getHeight() {
switch (widget.size) {
case ButtonSize.small:
return 32;
case ButtonSize.medium:
return 44;
case ButtonSize.large:
return 56;
case ButtonSize.extraLarge:
return 72;
}
}
EdgeInsets _getPadding() {
switch (widget.size) {
case ButtonSize.small:
return const EdgeInsets.symmetric(horizontal: 16, vertical: 6);
case ButtonSize.medium:
return const EdgeInsets.symmetric(horizontal: 24, vertical: 12);
case ButtonSize.large:
return const EdgeInsets.symmetric(horizontal: 32, vertical: 16);
case ButtonSize.extraLarge:
return const EdgeInsets.symmetric(horizontal: 40, vertical: 20);
}
}
double _getFontSize() {
switch (widget.size) {
case ButtonSize.small:
return 14;
case ButtonSize.medium:
return 16;
case ButtonSize.large:
return 18;
case ButtonSize.extraLarge:
return 20;
}
}
double _getIconSize() {
switch (widget.size) {
case ButtonSize.small:
return 16;
case ButtonSize.medium:
return 20;
case ButtonSize.large:
return 24;
case ButtonSize.extraLarge:
return 28;
}
}
double _getIconSpacing() {
switch (widget.size) {
case ButtonSize.small:
return 6;
case ButtonSize.medium:
return 8;
case ButtonSize.large:
return 10;
case ButtonSize.extraLarge:
return 12;
}
}
TextStyle _getTextStyle(_ButtonConfig config) {
return TextStyle(
fontSize: _getFontSize(),
fontWeight: FontWeight.w600,
color: config.foregroundColor,
letterSpacing: 0.5,
);
}
void _handleTap() {
if (widget.hapticFeedback) {
HapticFeedback.lightImpact();
}
widget.onPressed?.call();
}
}
class _ButtonConfig {
final Color backgroundColor;
final Color foregroundColor;
final Color? borderColor;
final bool hasElevation;
final bool useGradient;
final bool isGlass;
_ButtonConfig({
required this.backgroundColor,
required this.foregroundColor,
this.borderColor,
this.hasElevation = false,
this.useGradient = false,
this.isGlass = false,
});
}

View File

@@ -1,411 +0,0 @@
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,
});
}

View File

@@ -1,322 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../theme/app_theme.dart';
enum CardVariant {
elevated,
outlined,
filled,
glass,
gradient,
}
enum CardSize {
compact,
standard,
expanded,
}
class SophisticatedCard extends StatefulWidget {
final Widget child;
final CardVariant variant;
final CardSize size;
final Color? backgroundColor;
final Color? borderColor;
final Gradient? gradient;
final List<BoxShadow>? customShadow;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final bool animated;
final bool showRipple;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? elevation;
final BorderRadius? borderRadius;
final Widget? header;
final Widget? footer;
final bool blurBackground;
const SophisticatedCard({
super.key,
required this.child,
this.variant = CardVariant.elevated,
this.size = CardSize.standard,
this.backgroundColor,
this.borderColor,
this.gradient,
this.customShadow,
this.onTap,
this.onLongPress,
this.animated = true,
this.showRipple = true,
this.padding,
this.margin,
this.elevation,
this.borderRadius,
this.header,
this.footer,
this.blurBackground = false,
});
@override
State<SophisticatedCard> createState() => _SophisticatedCardState();
}
class _SophisticatedCardState extends State<SophisticatedCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _shadowAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.98,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_shadowAnimation = Tween<double>(
begin: 1.0,
end: 0.7,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final borderRadius = widget.borderRadius ?? _getDefaultBorderRadius();
final padding = widget.padding ?? _getDefaultPadding();
final margin = widget.margin ?? EdgeInsets.zero;
Widget card = AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: widget.animated ? _scaleAnimation.value : 1.0,
child: Container(
margin: margin,
decoration: _getDecoration(borderRadius),
child: ClipRRect(
borderRadius: borderRadius,
child: Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.header != null) ...[
widget.header!,
const Divider(height: 1),
],
Flexible(
child: Padding(
padding: padding,
child: widget.child,
),
),
if (widget.footer != null) ...[
const Divider(height: 1),
widget.footer!,
],
],
),
),
),
),
);
},
);
if (widget.onTap != null || widget.onLongPress != null) {
card = InkWell(
onTap: widget.onTap != null ? _handleTap : null,
onLongPress: widget.onLongPress,
onTapDown: widget.animated ? (_) => _animationController.forward() : null,
onTapUp: widget.animated ? (_) => _animationController.reverse() : null,
onTapCancel: widget.animated ? () => _animationController.reverse() : null,
borderRadius: borderRadius,
splashColor: widget.showRipple ? null : Colors.transparent,
highlightColor: widget.showRipple ? null : Colors.transparent,
child: card,
);
}
return card;
}
EdgeInsets _getDefaultPadding() {
switch (widget.size) {
case CardSize.compact:
return const EdgeInsets.all(12);
case CardSize.standard:
return const EdgeInsets.all(16);
case CardSize.expanded:
return const EdgeInsets.all(24);
}
}
BorderRadius _getDefaultBorderRadius() {
switch (widget.size) {
case CardSize.compact:
return BorderRadius.circular(12);
case CardSize.standard:
return BorderRadius.circular(16);
case CardSize.expanded:
return BorderRadius.circular(20);
}
}
double _getDefaultElevation() {
switch (widget.variant) {
case CardVariant.elevated:
return widget.elevation ?? 8;
case CardVariant.glass:
return 12;
default:
return 0;
}
}
Decoration _getDecoration(BorderRadius borderRadius) {
final elevation = _getDefaultElevation();
switch (widget.variant) {
case CardVariant.elevated:
return BoxDecoration(
color: widget.backgroundColor ?? Colors.white,
borderRadius: borderRadius,
boxShadow: widget.customShadow ?? [
BoxShadow(
color: Colors.black.withOpacity(0.1 * _shadowAnimation.value),
blurRadius: elevation * _shadowAnimation.value,
offset: Offset(0, elevation * 0.5 * _shadowAnimation.value),
),
],
);
case CardVariant.outlined:
return BoxDecoration(
color: widget.backgroundColor ?? Colors.white,
borderRadius: borderRadius,
border: Border.all(
color: widget.borderColor ?? AppTheme.textHint.withOpacity(0.2),
width: 1,
),
);
case CardVariant.filled:
return BoxDecoration(
color: widget.backgroundColor ?? AppTheme.backgroundLight,
borderRadius: borderRadius,
);
case CardVariant.glass:
return BoxDecoration(
color: (widget.backgroundColor ?? Colors.white).withOpacity(0.9),
borderRadius: borderRadius,
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1 * _shadowAnimation.value),
blurRadius: 20 * _shadowAnimation.value,
offset: Offset(0, 8 * _shadowAnimation.value),
),
],
);
case CardVariant.gradient:
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [
widget.backgroundColor ?? AppTheme.primaryColor,
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: borderRadius,
boxShadow: [
BoxShadow(
color: (widget.backgroundColor ?? AppTheme.primaryColor)
.withOpacity(0.3 * _shadowAnimation.value),
blurRadius: 15 * _shadowAnimation.value,
offset: Offset(0, 8 * _shadowAnimation.value),
),
],
);
}
}
void _handleTap() {
if (widget.animated) {
HapticFeedback.lightImpact();
}
widget.onTap?.call();
}
}
// Predefined card variants
class ElevatedCard extends SophisticatedCard {
const ElevatedCard({
super.key,
required super.child,
super.onTap,
super.padding,
super.margin,
super.elevation,
}) : super(variant: CardVariant.elevated);
}
class OutlinedCard extends SophisticatedCard {
const OutlinedCard({
super.key,
required super.child,
super.onTap,
super.padding,
super.margin,
super.borderColor,
}) : super(variant: CardVariant.outlined);
}
class GlassCard extends SophisticatedCard {
const GlassCard({
super.key,
required super.child,
super.onTap,
super.padding,
super.margin,
}) : super(variant: CardVariant.glass);
}
class GradientCard extends SophisticatedCard {
const GradientCard({
super.key,
required super.child,
super.onTap,
super.padding,
super.margin,
super.gradient,
super.backgroundColor,
}) : super(variant: CardVariant.gradient);
}

View File

@@ -1,340 +0,0 @@
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,
}

View File

@@ -1,220 +0,0 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class ComingSoonPage extends StatelessWidget {
final String title;
final String description;
final IconData icon;
final Color color;
final List<String>? features;
const ComingSoonPage({
super.key,
required this.title,
required this.description,
required this.icon,
required this.color,
this.features,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom - 48, // padding
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône principale
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color,
color.withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(60),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Icon(
icon,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 32),
// Titre
Text(
title,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Description
Text(
description,
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Fonctionnalités à venir (si fournies)
if (features != null) ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.upcoming,
color: color,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Fonctionnalités à venir',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
...features!.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
feature,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
),
],
),
)).toList(),
],
),
),
const SizedBox(height: 32),
],
// Badge "En développement"
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.infoColor,
AppTheme.infoColor.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: AppTheme.infoColor.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.construction,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
const Text(
'En cours de développement',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 24),
// Message d'encouragement
Text(
'Cette fonctionnalité sera bientôt disponible.\nMerci pour votre patience !',
style: TextStyle(
fontSize: 14,
color: AppTheme.textHint,
height: 1.4,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
);
}
}

View File

@@ -1,239 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
/// Layout de page unifié pour toutes les features de l'application
///
/// Fournit une structure cohérente avec :
/// - AppBar standardisée avec actions personnalisables
/// - Body avec padding et scroll automatique
/// - FloatingActionButton optionnel
/// - Gestion des états de chargement et d'erreur
class UnifiedPageLayout extends StatelessWidget {
/// Titre de la page affiché dans l'AppBar
final String title;
/// Sous-titre optionnel affiché sous le titre
final String? subtitle;
/// Icône principale de la page
final IconData? icon;
/// Couleur de l'icône (par défaut : primaryColor)
final Color? iconColor;
/// Actions personnalisées dans l'AppBar
final List<Widget>? actions;
/// Contenu principal de la page
final Widget body;
/// FloatingActionButton optionnel
final Widget? floatingActionButton;
/// Position du FloatingActionButton
final FloatingActionButtonLocation? floatingActionButtonLocation;
/// Indique si la page est en cours de chargement
final bool isLoading;
/// Message d'erreur à afficher
final String? errorMessage;
/// Callback pour rafraîchir la page
final VoidCallback? onRefresh;
/// Padding personnalisé pour le body (par défaut : 16.0)
final EdgeInsetsGeometry? padding;
/// Indique si le body doit être scrollable (par défaut : true)
final bool scrollable;
/// Couleur de fond personnalisée
final Color? backgroundColor;
/// Indique si l'AppBar doit être affichée (par défaut : true)
final bool showAppBar;
const UnifiedPageLayout({
super.key,
required this.title,
required this.body,
this.subtitle,
this.icon,
this.iconColor,
this.actions,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.isLoading = false,
this.errorMessage,
this.onRefresh,
this.padding,
this.scrollable = true,
this.backgroundColor,
this.showAppBar = true,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor ?? AppTheme.backgroundLight,
appBar: showAppBar ? _buildAppBar(context) : null,
body: _buildBody(context),
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 1,
surfaceTintColor: Colors.white,
title: Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: iconColor ?? AppTheme.primaryColor,
size: 24,
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
if (subtitle != null)
Text(
subtitle!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
actions: actions,
);
}
Widget _buildBody(BuildContext context) {
Widget content = body;
// Gestion des états d'erreur
if (errorMessage != null) {
content = _buildErrorState(context);
}
// Gestion de l'état de chargement
else if (isLoading) {
content = _buildLoadingState();
}
// Application du padding
if (padding != null || (padding == null && scrollable)) {
content = Padding(
padding: padding ?? const EdgeInsets.all(16.0),
child: content,
);
}
// Gestion du scroll
if (scrollable && errorMessage == null && !isLoading) {
if (onRefresh != null) {
content = RefreshIndicator(
onRefresh: () async => onRefresh!(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: content,
),
);
} else {
content = SingleChildScrollView(child: content);
}
}
return SafeArea(child: content);
}
Widget _buildLoadingState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
SizedBox(height: 16),
Text(
'Chargement...',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
);
}
Widget _buildErrorState(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
Text(
'Une erreur est survenue',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 24),
if (onRefresh != null)
ElevatedButton.icon(
onPressed: onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}

View File

@@ -1,248 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
class CustomTextField extends StatefulWidget {
final TextEditingController controller;
final String label;
final String? hintText;
final IconData? prefixIcon;
final Widget? suffixIcon;
final bool obscureText;
final TextInputType keyboardType;
final TextInputAction textInputAction;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final bool enabled;
final int maxLines;
final int? maxLength;
final List<TextInputFormatter>? inputFormatters;
final bool autofocus;
const CustomTextField({
super.key,
required this.controller,
required this.label,
this.hintText,
this.prefixIcon,
this.suffixIcon,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done,
this.validator,
this.onChanged,
this.onFieldSubmitted,
this.enabled = true,
this.maxLines = 1,
this.maxLength,
this.inputFormatters,
this.autofocus = false,
});
@override
State<CustomTextField> createState() => _CustomTextFieldState();
}
class _CustomTextFieldState extends State<CustomTextField>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<Color?> _borderColorAnimation;
late Animation<Color?> _labelColorAnimation;
bool _isFocused = false;
String? _errorText;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_borderColorAnimation = ColorTween(
begin: AppTheme.borderColor,
end: AppTheme.primaryColor,
).animate(_animationController);
_labelColorAnimation = ColorTween(
begin: AppTheme.textSecondary,
end: AppTheme.primaryColor,
).animate(_animationController);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
widget.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _labelColorAnimation.value,
),
),
),
// Champ de saisie
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: _isFocused
? [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: TextFormField(
controller: widget.controller,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
enabled: widget.enabled,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
inputFormatters: widget.inputFormatters,
autofocus: widget.autofocus,
validator: (value) {
final error = widget.validator?.call(value);
setState(() {
_errorText = error;
});
return error;
},
onChanged: widget.onChanged,
onFieldSubmitted: widget.onFieldSubmitted,
onTap: () {
setState(() {
_isFocused = true;
});
_animationController.forward();
},
onTapOutside: (_) {
setState(() {
_isFocused = false;
});
_animationController.reverse();
FocusScope.of(context).unfocus();
},
style: const TextStyle(
fontSize: 16,
color: AppTheme.textPrimary,
),
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: const TextStyle(
color: AppTheme.textHint,
fontSize: 16,
),
prefixIcon: widget.prefixIcon != null
? Icon(
widget.prefixIcon,
color: _isFocused
? AppTheme.primaryColor
: AppTheme.textHint,
)
: null,
suffixIcon: widget.suffixIcon,
filled: true,
fillColor: widget.enabled
? Colors.white
: AppTheme.backgroundLight,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: _borderColorAnimation.value ?? AppTheme.borderColor,
width: _isFocused ? 2 : 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: _errorText != null
? AppTheme.errorColor
: AppTheme.borderColor,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: _errorText != null
? AppTheme.errorColor
: AppTheme.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
counterText: '',
),
),
),
// Message d'erreur
if (_errorText != null)
Padding(
padding: const EdgeInsets.only(top: 8, left: 4),
child: Row(
children: [
const Icon(
Icons.error_outline,
size: 16,
color: AppTheme.errorColor,
),
const SizedBox(width: 6),
Expanded(
child: Text(
_errorText!,
style: const TextStyle(
color: AppTheme.errorColor,
fontSize: 12,
),
),
),
],
),
),
],
);
},
);
}
}

View File

@@ -1,371 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../cards/unified_card_widget.dart';
/// Widget de liste unifié avec animations et gestion d'états
///
/// Fournit :
/// - Animations d'apparition staggerées
/// - Gestion du scroll infini
/// - États de chargement et d'erreur
/// - Refresh-to-reload
/// - Séparateurs personnalisables
class UnifiedListWidget<T> extends StatefulWidget {
/// Liste des éléments à afficher
final List<T> items;
/// Builder pour chaque élément de la liste
final Widget Function(BuildContext context, T item, int index) itemBuilder;
/// Indique si la liste est en cours de chargement
final bool isLoading;
/// Indique si tous les éléments ont été chargés (pour le scroll infini)
final bool hasReachedMax;
/// Callback pour charger plus d'éléments
final VoidCallback? onLoadMore;
/// Callback pour rafraîchir la liste
final Future<void> Function()? onRefresh;
/// Message d'erreur à afficher
final String? errorMessage;
/// Callback pour réessayer en cas d'erreur
final VoidCallback? onRetry;
/// Widget à afficher quand la liste est vide
final Widget? emptyWidget;
/// Message à afficher quand la liste est vide
final String? emptyMessage;
/// Icône à afficher quand la liste est vide
final IconData? emptyIcon;
/// Padding de la liste
final EdgeInsetsGeometry? padding;
/// Espacement entre les éléments
final double itemSpacing;
/// Indique si les animations d'apparition sont activées
final bool enableAnimations;
/// Durée de l'animation d'apparition de chaque élément
final Duration animationDuration;
/// Délai entre les animations d'éléments
final Duration animationDelay;
/// Contrôleur de scroll personnalisé
final ScrollController? scrollController;
/// Physics du scroll
final ScrollPhysics? physics;
const UnifiedListWidget({
super.key,
required this.items,
required this.itemBuilder,
this.isLoading = false,
this.hasReachedMax = false,
this.onLoadMore,
this.onRefresh,
this.errorMessage,
this.onRetry,
this.emptyWidget,
this.emptyMessage,
this.emptyIcon,
this.padding,
this.itemSpacing = 12.0,
this.enableAnimations = true,
this.animationDuration = const Duration(milliseconds: 300),
this.animationDelay = const Duration(milliseconds: 100),
this.scrollController,
this.physics,
});
@override
State<UnifiedListWidget<T>> createState() => _UnifiedListWidgetState<T>();
}
class _UnifiedListWidgetState<T> extends State<UnifiedListWidget<T>>
with TickerProviderStateMixin {
late ScrollController _scrollController;
late AnimationController _listAnimationController;
List<AnimationController> _itemControllers = [];
List<Animation<double>> _itemAnimations = [];
List<Animation<Offset>> _slideAnimations = [];
@override
void initState() {
super.initState();
_scrollController = widget.scrollController ?? ScrollController();
_scrollController.addListener(_onScroll);
_listAnimationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_initializeItemAnimations();
if (widget.enableAnimations) {
_startAnimations();
}
}
@override
void didUpdateWidget(UnifiedListWidget<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.items.length != oldWidget.items.length) {
_updateItemAnimations();
}
}
@override
void dispose() {
if (widget.scrollController == null) {
_scrollController.dispose();
}
_listAnimationController.dispose();
for (final controller in _itemControllers) {
controller.dispose();
}
super.dispose();
}
void _initializeItemAnimations() {
if (!widget.enableAnimations) return;
_updateItemAnimations();
}
void _updateItemAnimations() {
if (!widget.enableAnimations) return;
// Dispose des anciens controllers s'ils existent
if (_itemControllers.isNotEmpty) {
for (final controller in _itemControllers) {
controller.dispose();
}
}
// Créer de nouveaux controllers pour chaque élément
_itemControllers = List.generate(
widget.items.length,
(index) => AnimationController(
duration: widget.animationDuration,
vsync: this,
),
);
// Animations de fade et scale
_itemAnimations = _itemControllers.map((controller) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
// Animations de slide depuis le bas
_slideAnimations = _itemControllers.map((controller) {
return Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
}
void _startAnimations() {
if (!widget.enableAnimations) return;
_listAnimationController.forward();
// Démarrer les animations des éléments avec un délai
for (int i = 0; i < _itemControllers.length; i++) {
Future.delayed(widget.animationDelay * i, () {
if (mounted && i < _itemControllers.length) {
_itemControllers[i].forward();
}
});
}
}
void _onScroll() {
if (_isBottom && widget.onLoadMore != null && !widget.isLoading && !widget.hasReachedMax) {
widget.onLoadMore!();
}
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
@override
Widget build(BuildContext context) {
// Gestion de l'état d'erreur
if (widget.errorMessage != null) {
return _buildErrorState();
}
// Gestion de l'état vide
if (widget.items.isEmpty && !widget.isLoading) {
return widget.emptyWidget ?? _buildEmptyState();
}
Widget listView = ListView.separated(
controller: _scrollController,
physics: widget.physics ?? const AlwaysScrollableScrollPhysics(),
padding: widget.padding ?? const EdgeInsets.all(16),
itemCount: widget.items.length + (widget.isLoading ? 1 : 0),
separatorBuilder: (context, index) => SizedBox(height: widget.itemSpacing),
itemBuilder: (context, index) {
// Indicateur de chargement en bas de liste
if (index >= widget.items.length) {
return _buildLoadingIndicator();
}
final item = widget.items[index];
Widget itemWidget = widget.itemBuilder(context, item, index);
// Application des animations si activées
if (widget.enableAnimations && index < _itemAnimations.length) {
itemWidget = AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return FadeTransition(
opacity: _itemAnimations[index],
child: SlideTransition(
position: _slideAnimations[index],
child: Transform.scale(
scale: 0.8 + (0.2 * _itemAnimations[index].value),
child: child,
),
),
);
},
child: itemWidget,
);
}
return itemWidget;
},
);
// Ajout du RefreshIndicator si onRefresh est fourni
if (widget.onRefresh != null) {
listView = RefreshIndicator(
onRefresh: widget.onRefresh!,
child: listView,
);
}
return listView;
}
Widget _buildLoadingIndicator() {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: AppTheme.textSecondary.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'Aucun élément',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary.withOpacity(0.7),
),
),
const SizedBox(height: 8),
Text(
'La liste est vide pour le moment',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
],
),
),
);
}
Widget _buildErrorState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
const Text(
'Une erreur est survenue',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
widget.errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 24),
if (widget.onRetry != null)
ElevatedButton.icon(
onPressed: widget.onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}

View File

@@ -1,203 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
class LoadingButton extends StatefulWidget {
final VoidCallback? onPressed;
final String text;
final bool isLoading;
final double? width;
final double height;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
final bool enabled;
const LoadingButton({
super.key,
required this.onPressed,
required this.text,
this.isLoading = false,
this.width,
this.height = 48,
this.backgroundColor,
this.textColor,
this.icon,
this.enabled = true,
});
@override
State<LoadingButton> createState() => _LoadingButtonState();
}
class _LoadingButtonState extends State<LoadingButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_opacityAnimation = Tween<double>(
begin: 1.0,
end: 0.8,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool get _isEnabled => widget.enabled && !widget.isLoading && widget.onPressed != null;
Color get _backgroundColor {
if (!_isEnabled) {
return AppTheme.textHint.withOpacity(0.3);
}
return widget.backgroundColor ?? AppTheme.primaryColor;
}
Color get _textColor {
if (!_isEnabled) {
return AppTheme.textHint;
}
return widget.textColor ?? Colors.white;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: _isEnabled
? [
BoxShadow(
color: _backgroundColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: null,
),
child: ElevatedButton(
onPressed: _isEnabled ? _handlePressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: _backgroundColor,
foregroundColor: _textColor,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24),
),
child: _buildButtonContent(),
),
),
),
);
},
);
}
Widget _buildButtonContent() {
if (widget.isLoading) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(_textColor),
strokeWidth: 2,
),
),
const SizedBox(width: 12),
Text(
'Chargement...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _textColor,
),
),
],
);
}
if (widget.icon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.icon,
size: 20,
color: _textColor,
),
const SizedBox(width: 8),
Text(
widget.text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _textColor,
),
),
],
);
}
return Text(
widget.text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _textColor,
),
);
}
void _handlePressed() {
if (!_isEnabled) return;
// Animation de pression
_animationController.forward().then((_) {
_animationController.reverse();
});
// Vibration tactile
HapticFeedback.lightImpact();
// Callback
widget.onPressed?.call();
}
}

View File

@@ -1,376 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../../../core/performance/performance_optimizer.dart';
/// ListView optimisé avec lazy loading intelligent et gestion de performance
///
/// Fonctionnalités :
/// - Lazy loading avec seuil configurable
/// - Recyclage automatique des widgets
/// - Animations optimisées
/// - Gestion mémoire intelligente
/// - Monitoring des performances
class OptimizedListView<T> extends StatefulWidget {
/// Liste des éléments à afficher
final List<T> items;
/// Builder pour chaque élément
final Widget Function(BuildContext context, T item, int index) itemBuilder;
/// Callback pour charger plus d'éléments
final Future<void> Function()? onLoadMore;
/// Callback pour rafraîchir la liste
final Future<void> Function()? onRefresh;
/// Indique si plus d'éléments peuvent être chargés
final bool hasMore;
/// Indique si le chargement est en cours
final bool isLoading;
/// Seuil pour déclencher le chargement (nombre d'éléments avant la fin)
final int loadMoreThreshold;
/// Hauteur estimée d'un élément (pour l'optimisation)
final double? itemExtent;
/// Padding de la liste
final EdgeInsetsGeometry? padding;
/// Séparateur entre les éléments
final Widget? separator;
/// Widget affiché quand la liste est vide
final Widget? emptyWidget;
/// Widget de chargement personnalisé
final Widget? loadingWidget;
/// Activer les animations
final bool enableAnimations;
/// Durée des animations
final Duration animationDuration;
/// Contrôleur de scroll personnalisé
final ScrollController? scrollController;
/// Physics du scroll
final ScrollPhysics? physics;
/// Activer le recyclage des widgets
final bool enableRecycling;
/// Nombre maximum de widgets en cache
final int maxCachedWidgets;
const OptimizedListView({
super.key,
required this.items,
required this.itemBuilder,
this.onLoadMore,
this.onRefresh,
this.hasMore = true,
this.isLoading = false,
this.loadMoreThreshold = 3,
this.itemExtent,
this.padding,
this.separator,
this.emptyWidget,
this.loadingWidget,
this.enableAnimations = true,
this.animationDuration = const Duration(milliseconds: 300),
this.scrollController,
this.physics,
this.enableRecycling = true,
this.maxCachedWidgets = 50,
});
@override
State<OptimizedListView<T>> createState() => _OptimizedListViewState<T>();
}
class _OptimizedListViewState<T> extends State<OptimizedListView<T>>
with TickerProviderStateMixin {
late ScrollController _scrollController;
late AnimationController _animationController;
/// Cache des widgets recyclés
final Map<String, Widget> _widgetCache = {};
/// Performance optimizer instance
final _optimizer = PerformanceOptimizer();
/// Indique si le chargement est en cours
bool _isLoadingMore = false;
@override
void initState() {
super.initState();
_scrollController = widget.scrollController ?? ScrollController();
_animationController = PerformanceOptimizer.createOptimizedController(
duration: widget.animationDuration,
vsync: this,
debugLabel: 'OptimizedListView',
);
// Écouter le scroll pour le lazy loading
_scrollController.addListener(_onScroll);
// Démarrer les animations si activées
if (widget.enableAnimations) {
_animationController.forward();
}
_optimizer.startTimer('list_build');
}
@override
void dispose() {
if (widget.scrollController == null) {
_scrollController.dispose();
}
_animationController.dispose();
_widgetCache.clear();
_optimizer.stopTimer('list_build');
super.dispose();
}
void _onScroll() {
if (!_scrollController.hasClients) return;
final position = _scrollController.position;
final maxScroll = position.maxScrollExtent;
final currentScroll = position.pixels;
// Calculer si on approche de la fin
final threshold = maxScroll - (widget.loadMoreThreshold * (widget.itemExtent ?? 100));
if (currentScroll >= threshold &&
widget.hasMore &&
!_isLoadingMore &&
widget.onLoadMore != null) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoadingMore) return;
setState(() {
_isLoadingMore = true;
});
_optimizer.startTimer('load_more');
try {
await widget.onLoadMore!();
} finally {
if (mounted) {
setState(() {
_isLoadingMore = false;
});
}
_optimizer.stopTimer('load_more');
}
}
Widget _buildOptimizedItem(BuildContext context, int index) {
if (index >= widget.items.length) {
// Widget de chargement en fin de liste
return _buildLoadingIndicator();
}
final item = widget.items[index];
final cacheKey = 'item_${item.hashCode}_$index';
// Utiliser le cache si le recyclage est activé
if (widget.enableRecycling && _widgetCache.containsKey(cacheKey)) {
_optimizer.incrementCounter('cache_hit');
return _widgetCache[cacheKey]!;
}
// Construire le widget
Widget itemWidget = widget.itemBuilder(context, item, index);
// Optimiser le widget
itemWidget = PerformanceOptimizer.optimizeWidget(
itemWidget,
key: 'optimized_$index',
forceRepaintBoundary: true,
);
// Ajouter les animations si activées
if (widget.enableAnimations) {
itemWidget = _buildAnimatedItem(itemWidget, index);
}
// Mettre en cache si le recyclage est activé
if (widget.enableRecycling) {
_cacheWidget(cacheKey, itemWidget);
}
_optimizer.incrementCounter('item_built');
return itemWidget;
}
Widget _buildAnimatedItem(Widget child, int index) {
final delay = Duration(milliseconds: (index * 50).clamp(0, 500));
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final animationValue = Curves.easeOutCubic.transform(
(_animationController.value - (delay.inMilliseconds / widget.animationDuration.inMilliseconds))
.clamp(0.0, 1.0),
);
return Transform.translate(
offset: Offset(0, 50 * (1 - animationValue)),
child: Opacity(
opacity: animationValue,
child: child,
),
);
},
);
}
void _cacheWidget(String key, Widget widget) {
// Limiter la taille du cache
if (_widgetCache.length >= widget.maxCachedWidgets) {
// Supprimer les plus anciens (simple FIFO)
final oldestKey = _widgetCache.keys.first;
_widgetCache.remove(oldestKey);
}
_widgetCache[key] = widget;
}
Widget _buildLoadingIndicator() {
return widget.loadingWidget ??
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
Widget _buildEmptyState() {
return widget.emptyWidget ??
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
'Aucun élément à afficher',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
// Liste vide
if (widget.items.isEmpty && !widget.isLoading) {
return _buildEmptyState();
}
// Calculer le nombre total d'éléments (items + indicateur de chargement)
final itemCount = widget.items.length + (widget.hasMore && _isLoadingMore ? 1 : 0);
Widget listView;
if (widget.separator != null) {
// ListView avec séparateurs
listView = ListView.separated(
controller: _scrollController,
physics: widget.physics,
padding: widget.padding,
itemCount: itemCount,
itemBuilder: _buildOptimizedItem,
separatorBuilder: (context, index) => widget.separator!,
);
} else {
// ListView standard
listView = ListView.builder(
controller: _scrollController,
physics: widget.physics,
padding: widget.padding,
itemCount: itemCount,
itemExtent: widget.itemExtent,
itemBuilder: _buildOptimizedItem,
);
}
// Ajouter RefreshIndicator si onRefresh est fourni
if (widget.onRefresh != null) {
listView = RefreshIndicator(
onRefresh: widget.onRefresh!,
child: listView,
);
}
return listView;
}
}
/// Extension pour faciliter l'utilisation
extension OptimizedListViewExtension<T> on List<T> {
/// Crée un OptimizedListView à partir de cette liste
Widget toOptimizedListView({
required Widget Function(BuildContext context, T item, int index) itemBuilder,
Future<void> Function()? onLoadMore,
Future<void> Function()? onRefresh,
bool hasMore = false,
bool isLoading = false,
int loadMoreThreshold = 3,
double? itemExtent,
EdgeInsetsGeometry? padding,
Widget? separator,
Widget? emptyWidget,
Widget? loadingWidget,
bool enableAnimations = true,
Duration animationDuration = const Duration(milliseconds: 300),
ScrollController? scrollController,
ScrollPhysics? physics,
bool enableRecycling = true,
int maxCachedWidgets = 50,
}) {
return OptimizedListView<T>(
items: this,
itemBuilder: itemBuilder,
onLoadMore: onLoadMore,
onRefresh: onRefresh,
hasMore: hasMore,
isLoading: isLoading,
loadMoreThreshold: loadMoreThreshold,
itemExtent: itemExtent,
padding: padding,
separator: separator,
emptyWidget: emptyWidget,
loadingWidget: loadingWidget,
enableAnimations: enableAnimations,
animationDuration: animationDuration,
scrollController: scrollController,
physics: physics,
enableRecycling: enableRecycling,
maxCachedWidgets: maxCachedWidgets,
);
}
}

View File

@@ -1,330 +0,0 @@
import 'package:flutter/material.dart';
import '../../core/auth/services/permission_service.dart';
/// Widget qui affiche son contenu seulement si l'utilisateur a les permissions requises
class PermissionWidget extends StatelessWidget {
const PermissionWidget({
super.key,
required this.child,
this.permission,
this.roles,
this.fallback,
this.showFallbackMessage = false,
this.fallbackMessage,
}) : assert(permission != null || roles != null, 'Either permission or roles must be provided');
/// Widget à afficher si les permissions sont accordées
final Widget child;
/// Fonction de vérification de permission personnalisée
final bool Function()? permission;
/// Liste des rôles autorisés
final List<String>? roles;
/// Widget à afficher si les permissions ne sont pas accordées
final Widget? fallback;
/// Afficher un message par défaut si pas de permissions
final bool showFallbackMessage;
/// Message personnalisé à afficher si pas de permissions
final String? fallbackMessage;
@override
Widget build(BuildContext context) {
final permissionService = PermissionService();
bool hasPermission = false;
if (permission != null) {
hasPermission = permission!();
} else if (roles != null) {
hasPermission = permissionService.hasAnyRole(roles!);
}
if (hasPermission) {
return child;
}
// Si pas de permissions, afficher le fallback ou rien
if (fallback != null) {
return fallback!;
}
if (showFallbackMessage) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 8),
Text(
fallbackMessage ?? 'Accès restreint',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
);
}
return const SizedBox.shrink();
}
}
/// Widget pour les boutons avec contrôle de permissions
class PermissionButton extends StatelessWidget {
const PermissionButton({
super.key,
required this.onPressed,
required this.child,
this.permission,
this.roles,
this.tooltip,
this.style,
this.showDisabled = true,
this.disabledMessage,
}) : assert(permission != null || roles != null, 'Either permission or roles must be provided');
/// Callback quand le bouton est pressé
final VoidCallback onPressed;
/// Contenu du bouton
final Widget child;
/// Fonction de vérification de permission personnalisée
final bool Function()? permission;
/// Liste des rôles autorisés
final List<String>? roles;
/// Tooltip du bouton
final String? tooltip;
/// Style du bouton
final ButtonStyle? style;
/// Afficher le bouton désactivé si pas de permissions
final bool showDisabled;
/// Message à afficher quand le bouton est désactivé
final String? disabledMessage;
@override
Widget build(BuildContext context) {
final permissionService = PermissionService();
bool hasPermission = false;
if (permission != null) {
hasPermission = permission!();
} else if (roles != null) {
hasPermission = permissionService.hasAnyRole(roles!);
}
if (!hasPermission && !showDisabled) {
return const SizedBox.shrink();
}
Widget button = ElevatedButton(
onPressed: hasPermission ? onPressed : null,
style: style,
child: child,
);
if (tooltip != null || (!hasPermission && disabledMessage != null)) {
button = Tooltip(
message: hasPermission
? (tooltip ?? '')
: (disabledMessage ?? 'Permissions insuffisantes'),
child: button,
);
}
return button;
}
}
/// Widget pour les IconButton avec contrôle de permissions
class PermissionIconButton extends StatelessWidget {
const PermissionIconButton({
super.key,
required this.onPressed,
required this.icon,
this.permission,
this.roles,
this.tooltip,
this.color,
this.showDisabled = true,
this.disabledMessage,
}) : assert(permission != null || roles != null, 'Either permission or roles must be provided');
/// Callback quand le bouton est pressé
final VoidCallback onPressed;
/// Icône du bouton
final Widget icon;
/// Fonction de vérification de permission personnalisée
final bool Function()? permission;
/// Liste des rôles autorisés
final List<String>? roles;
/// Tooltip du bouton
final String? tooltip;
/// Couleur de l'icône
final Color? color;
/// Afficher le bouton désactivé si pas de permissions
final bool showDisabled;
/// Message à afficher quand le bouton est désactivé
final String? disabledMessage;
@override
Widget build(BuildContext context) {
final permissionService = PermissionService();
bool hasPermission = false;
if (permission != null) {
hasPermission = permission!();
} else if (roles != null) {
hasPermission = permissionService.hasAnyRole(roles!);
}
if (!hasPermission && !showDisabled) {
return const SizedBox.shrink();
}
return IconButton(
onPressed: hasPermission ? onPressed : null,
icon: icon,
color: hasPermission ? color : Colors.grey,
tooltip: hasPermission
? tooltip
: (disabledMessage ?? 'Permissions insuffisantes'),
);
}
}
/// Widget pour les FloatingActionButton avec contrôle de permissions
class PermissionFAB extends StatelessWidget {
const PermissionFAB({
super.key,
required this.onPressed,
required this.child,
this.permission,
this.roles,
this.tooltip,
this.backgroundColor,
this.foregroundColor,
this.showDisabled = false,
}) : assert(permission != null || roles != null, 'Either permission or roles must be provided');
/// Callback quand le bouton est pressé
final VoidCallback onPressed;
/// Contenu du bouton
final Widget child;
/// Fonction de vérification de permission personnalisée
final bool Function()? permission;
/// Liste des rôles autorisés
final List<String>? roles;
/// Tooltip du bouton
final String? tooltip;
/// Couleur de fond
final Color? backgroundColor;
/// Couleur de premier plan
final Color? foregroundColor;
/// Afficher le bouton désactivé si pas de permissions
final bool showDisabled;
@override
Widget build(BuildContext context) {
final permissionService = PermissionService();
bool hasPermission = false;
if (permission != null) {
hasPermission = permission!();
} else if (roles != null) {
hasPermission = permissionService.hasAnyRole(roles!);
}
if (!hasPermission && !showDisabled) {
return const SizedBox.shrink();
}
return FloatingActionButton(
onPressed: hasPermission ? onPressed : null,
backgroundColor: hasPermission ? backgroundColor : Colors.grey,
foregroundColor: foregroundColor,
tooltip: tooltip,
child: child,
);
}
}
/// Mixin pour faciliter l'utilisation des permissions dans les widgets
mixin PermissionMixin {
PermissionService get permissionService => PermissionService();
/// Vérifie si l'utilisateur a une permission spécifique
bool hasPermission(bool Function() permission) {
return permission();
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
return permissionService.hasAnyRole(roles);
}
/// Affiche un SnackBar d'erreur de permission
void showPermissionError(BuildContext context, [String? message]) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message ?? 'Vous n\'avez pas les permissions nécessaires pour cette action',
),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'Fermer',
textColor: Colors.white,
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
),
),
);
}
/// Exécute une action seulement si l'utilisateur a les permissions
void executeWithPermission(
BuildContext context,
bool Function() permission,
VoidCallback action, {
String? errorMessage,
}) {
if (permission()) {
action();
} else {
showPermissionError(context, errorMessage);
}
}
}

View File

@@ -1,262 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../cards/unified_card_widget.dart';
/// Section KPI unifiée pour afficher des indicateurs clés
///
/// Fournit :
/// - Cartes KPI avec animations
/// - Layouts adaptatifs (grille ou liste)
/// - Indicateurs de tendance
/// - Couleurs thématiques
class UnifiedKPISection extends StatelessWidget {
/// Liste des KPI à afficher
final List<UnifiedKPIData> kpis;
/// Titre de la section
final String? title;
/// Nombre de colonnes dans la grille (par défaut : 2)
final int crossAxisCount;
/// Espacement entre les cartes
final double spacing;
/// Callback lors du tap sur un KPI
final void Function(UnifiedKPIData kpi)? onKPITap;
const UnifiedKPISection({
super.key,
required this.kpis,
this.title,
this.crossAxisCount = 2,
this.spacing = 16.0,
this.onKPITap,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Text(
title!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
],
_buildKPIGrid(),
],
);
}
Widget _buildKPIGrid() {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: 1.4,
),
itemCount: kpis.length,
itemBuilder: (context, index) {
final kpi = kpis[index];
return UnifiedCard.kpi(
onTap: onKPITap != null ? () => onKPITap!(kpi) : null,
child: _buildKPIContent(kpi),
);
},
);
}
Widget _buildKPIContent(UnifiedKPIData kpi) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// En-tête avec icône et titre
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: kpi.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
kpi.icon,
color: kpi.color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
kpi.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
// Valeur principale
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
kpi.value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (kpi.trend != null) ...[
const SizedBox(width: 8),
_buildTrendIndicator(kpi.trend!),
],
],
),
// Sous-titre ou description
if (kpi.subtitle != null) ...[
const SizedBox(height: 4),
Text(
kpi.subtitle!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
);
}
Widget _buildTrendIndicator(UnifiedKPITrend trend) {
IconData icon;
Color color;
switch (trend.direction) {
case UnifiedKPITrendDirection.up:
icon = Icons.trending_up;
color = AppTheme.successColor;
break;
case UnifiedKPITrendDirection.down:
icon = Icons.trending_down;
color = AppTheme.errorColor;
break;
case UnifiedKPITrendDirection.stable:
icon = Icons.trending_flat;
color = AppTheme.textSecondary;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 12,
color: color,
),
const SizedBox(width: 2),
Text(
trend.value,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
);
}
}
/// Données pour un KPI unifié
class UnifiedKPIData {
/// Titre du KPI
final String title;
/// Valeur principale à afficher
final String value;
/// Sous-titre ou description optionnelle
final String? subtitle;
/// Icône représentative
final IconData icon;
/// Couleur thématique
final Color color;
/// Indicateur de tendance optionnel
final UnifiedKPITrend? trend;
/// Données supplémentaires pour les callbacks
final Map<String, dynamic>? metadata;
const UnifiedKPIData({
required this.title,
required this.value,
required this.icon,
required this.color,
this.subtitle,
this.trend,
this.metadata,
});
}
/// Indicateur de tendance pour les KPI
class UnifiedKPITrend {
/// Direction de la tendance
final UnifiedKPITrendDirection direction;
/// Valeur de la tendance (ex: "+12%", "-5", "stable")
final String value;
/// Label descriptif de la tendance (ex: "ce mois", "vs mois dernier")
final String? label;
const UnifiedKPITrend({
required this.direction,
required this.value,
this.label,
});
}
/// Direction de tendance disponibles
enum UnifiedKPITrendDirection {
up,
down,
stable,
}

View File

@@ -1,262 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../cards/unified_card_widget.dart';
/// Section d'actions rapides unifiée
///
/// Fournit :
/// - Grille d'actions avec icônes
/// - Animations au tap
/// - Layouts adaptatifs
/// - Badges de notification
class UnifiedQuickActionsSection extends StatelessWidget {
/// Liste des actions rapides
final List<UnifiedQuickAction> actions;
/// Titre de la section
final String? title;
/// Nombre de colonnes dans la grille (par défaut : 3)
final int crossAxisCount;
/// Espacement entre les actions
final double spacing;
/// Callback lors du tap sur une action
final void Function(UnifiedQuickAction action)? onActionTap;
const UnifiedQuickActionsSection({
super.key,
required this.actions,
this.title,
this.crossAxisCount = 3,
this.spacing = 12.0,
this.onActionTap,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Text(
title!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
],
_buildActionsGrid(),
],
);
}
Widget _buildActionsGrid() {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: 1.0,
),
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return _buildActionCard(action);
},
);
}
Widget _buildActionCard(UnifiedQuickAction action) {
return UnifiedCard(
onTap: action.enabled && onActionTap != null
? () => onActionTap!(action)
: null,
variant: UnifiedCardVariant.outlined,
padding: const EdgeInsets.all(12),
child: Stack(
children: [
// Contenu principal
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône avec conteneur coloré
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: action.enabled
? action.color.withOpacity(0.1)
: AppTheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
action.icon,
color: action.enabled
? action.color
: AppTheme.textSecondary.withOpacity(0.5),
size: 24,
),
),
const SizedBox(height: 8),
// Titre de l'action
Text(
action.title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: action.enabled
? AppTheme.textPrimary
: AppTheme.textSecondary.withOpacity(0.5),
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
// Badge de notification
if (action.badgeCount != null && action.badgeCount! > 0)
Positioned(
top: 0,
right: 0,
child: _buildBadge(action.badgeCount!),
),
// Indicateur "nouveau"
if (action.isNew)
Positioned(
top: 4,
right: 4,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppTheme.accentColor,
shape: BoxShape.circle,
),
),
),
],
),
);
}
Widget _buildBadge(int count) {
final displayCount = count > 99 ? '99+' : count.toString();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.errorColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 2),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
displayCount,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
);
}
}
/// Données pour une action rapide unifiée
class UnifiedQuickAction {
/// Identifiant unique de l'action
final String id;
/// Titre de l'action
final String title;
/// Icône représentative
final IconData icon;
/// Couleur thématique
final Color color;
/// Indique si l'action est activée
final bool enabled;
/// Nombre de notifications/badges (optionnel)
final int? badgeCount;
/// Indique si l'action est nouvelle
final bool isNew;
/// Données supplémentaires pour les callbacks
final Map<String, dynamic>? metadata;
const UnifiedQuickAction({
required this.id,
required this.title,
required this.icon,
required this.color,
this.enabled = true,
this.badgeCount,
this.isNew = false,
this.metadata,
});
}
/// Actions rapides prédéfinies communes
class CommonQuickActions {
static const UnifiedQuickAction addMember = UnifiedQuickAction(
id: 'add_member',
title: 'Ajouter\nMembre',
icon: Icons.person_add,
color: AppTheme.primaryColor,
);
static const UnifiedQuickAction addEvent = UnifiedQuickAction(
id: 'add_event',
title: 'Nouvel\nÉvénement',
icon: Icons.event_available,
color: AppTheme.accentColor,
);
static const UnifiedQuickAction collectPayment = UnifiedQuickAction(
id: 'collect_payment',
title: 'Collecter\nCotisation',
icon: Icons.payment,
color: AppTheme.successColor,
);
static const UnifiedQuickAction sendMessage = UnifiedQuickAction(
id: 'send_message',
title: 'Envoyer\nMessage',
icon: Icons.message,
color: AppTheme.infoColor,
);
static const UnifiedQuickAction generateReport = UnifiedQuickAction(
id: 'generate_report',
title: 'Générer\nRapport',
icon: Icons.assessment,
color: AppTheme.warningColor,
);
static const UnifiedQuickAction manageSettings = UnifiedQuickAction(
id: 'manage_settings',
title: 'Paramètres',
icon: Icons.settings,
color: AppTheme.textSecondary,
);
}

View File

@@ -1,34 +0,0 @@
/// Fichier d'export pour tous les composants unifiés de l'application
///
/// Permet d'importer facilement tous les widgets standardisés :
/// ```dart
/// import 'package:unionflow_mobile_apps/shared/widgets/unified_components.dart';
/// ```
// Layouts et structures
export 'common/unified_page_layout.dart';
// Cartes et conteneurs
export 'cards/unified_card_widget.dart';
// Listes et grilles
export 'lists/unified_list_widget.dart';
// Boutons et interactions
export 'buttons/unified_button_set.dart';
// Sections communes
export 'sections/unified_kpi_section.dart';
export 'sections/unified_quick_actions_section.dart';
// Widgets existants réutilisables
export 'coming_soon_page.dart';
export 'custom_text_field.dart';
export 'loading_button.dart';
export 'permission_widget.dart';
// Sous-dossiers existants (commentés car certains fichiers n'existent pas encore)
// export 'avatars/avatar_widget.dart';
// export 'badges/status_badge.dart';
// export 'buttons/action_button.dart';
// export 'cards/info_card.dart';