405 lines
9.6 KiB
Dart
405 lines
9.6 KiB
Dart
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,
|
|
);
|
|
}
|
|
} |