Authentification stable - WIP
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user