baseproject

This commit is contained in:
Gaurav Kumar
2025-09-06 19:21:52 +05:30
commit 01be9df2ed
254 changed files with 28342 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
import 'package:base_project/view_model/system_params/system_params_view_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../core/providers/theme_provider.dart';
import '../theme_toggle.dart';
class ModernAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool automaticallyImplyLeading;
final Color? backgroundColor;
final Color? foregroundColor;
final double? elevation;
final bool centerTitle;
final Widget? titleWidget;
final VoidCallback? onMenuPressed;
final VoidCallback? onProfilePressed;
final String? userAvatar;
final ImageProvider<Object>? userAvatarImage;
final String? userName;
final bool showThemeToggle;
final bool showUserProfile;
final bool showLogoInTitle;
final bool showBackButton;
final ImageProvider<Object>? logoImage;
const ModernAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.automaticallyImplyLeading = true,
this.backgroundColor,
this.foregroundColor,
this.elevation,
this.centerTitle = true,
this.titleWidget,
this.onMenuPressed,
this.onProfilePressed,
this.userAvatar,
this.userAvatarImage,
this.userName,
this.showThemeToggle = true,
this.showUserProfile = true,
this.showLogoInTitle = true,
this.showBackButton = false,
this.logoImage,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDarkMode = theme.brightness == Brightness.dark;
final Widget computedTitle = _buildTitle(context, theme, colorScheme);
return AppBar(
title: computedTitle,
centerTitle: titleWidget != null
? centerTitle
: (showLogoInTitle ? false : centerTitle),
backgroundColor: backgroundColor ?? colorScheme.surface,
foregroundColor: foregroundColor ?? colorScheme.onSurface,
elevation: elevation ?? 0,
automaticallyImplyLeading: automaticallyImplyLeading,
leading: leading ??
(automaticallyImplyLeading
? (showBackButton
? IconButton(
icon: Icon(
Icons.arrow_back,
color: foregroundColor ?? colorScheme.onSurface,
),
onPressed: () {
Navigator.of(context).pop();
},
)
: IconButton(
icon: Icon(
Icons.menu,
color: foregroundColor ?? colorScheme.onSurface,
),
onPressed: onMenuPressed ??
() {
Scaffold.of(context).openDrawer();
},
))
: null),
actions: [
// Theme Toggle
if (showThemeToggle) ...[
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return ThemeToggle(
isDarkMode: themeProvider.isDarkMode,
onThemeChanged: (isDark) {
themeProvider.setTheme(isDark);
},
size: UIConstants.iconSizeMedium,
);
},
),
const SizedBox(width: UIConstants.spacing8),
],
// User Profile
if (showUserProfile) ...[
GestureDetector(
onTap: onProfilePressed,
child: Container(
margin: const EdgeInsets.only(right: UIConstants.spacing16),
padding: const EdgeInsets.all(UIConstants.spacing4),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 2,
),
),
child: CircleAvatar(
radius: UIConstants.iconSizeMedium / 2,
backgroundColor: colorScheme.primaryContainer,
backgroundImage: userAvatarImage ??
(userAvatar != null ? NetworkImage(userAvatar!) : null),
child: userAvatarImage == null && userAvatar == null
? Icon(
Icons.person,
color: colorScheme.onPrimaryContainer,
size: UIConstants.iconSizeMedium * 0.6,
)
: null,
),
),
),
],
// Custom Actions
if (actions != null) ...actions!,
],
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(UIConstants.radius16),
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
Widget _buildTitle(
BuildContext context, ThemeData theme, ColorScheme colorScheme) {
if (titleWidget != null) return titleWidget!;
final effectiveLogo = logoImage ?? _resolveLogoImage(context);
if (showLogoInTitle) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: (backgroundColor ?? colorScheme.surface),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.outline.withOpacity(0.15), width: 1),
),
clipBehavior: Clip.antiAlias,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: effectiveLogo != null
? Image(
image: effectiveLogo,
filterQuality: FilterQuality.high,
)
: Icon(
Icons.image_not_supported_outlined,
color: colorScheme.onSurface.withOpacity(0.5),
),
),
),
const SizedBox(width: UIConstants.spacing12),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: foregroundColor ?? colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
);
}
return Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: foregroundColor ?? colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
);
}
ImageProvider<Object>? _resolveLogoImage(BuildContext context) {
try {
final sysVm = Provider.of<SystemParamsViewModel>(context, listen: true);
if (sysVm.profileImageBytes != null &&
sysVm.profileImageBytes!.isNotEmpty) {
return MemoryImage(sysVm.profileImageBytes!);
}
} catch (_) {}
return const AssetImage('assets/images/image_not_found.png');
}
}

View File

@@ -0,0 +1,342 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
enum ModernButtonType {
primary,
secondary,
outline,
text,
danger,
}
enum ModernButtonSize {
small,
medium,
large,
}
class ModernButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;
final ModernButtonType type;
final ModernButtonSize size;
final bool isLoading;
final bool isDisabled;
final Widget? icon;
final bool isIconOnly;
final double? width;
final double? height;
final EdgeInsetsGeometry? padding;
final BorderRadius? borderRadius;
final String? tooltip;
const ModernButton({
super.key,
required this.text,
this.onPressed,
this.type = ModernButtonType.primary,
this.size = ModernButtonSize.medium,
this.isLoading = false,
this.isDisabled = false,
this.icon,
this.isIconOnly = false,
this.width,
this.height,
this.padding,
this.borderRadius,
this.tooltip,
});
@override
State<ModernButton> createState() => _ModernButtonState();
}
class _ModernButtonState extends State<ModernButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = true;
});
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
void _onTapCancel() {
if (!widget.isDisabled && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
bool get _canPress =>
!widget.isDisabled && !widget.isLoading && widget.onPressed != null;
Color _getBackgroundColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.surfaceVariant;
}
switch (widget.type) {
case ModernButtonType.primary:
return theme.colorScheme.primary;
case ModernButtonType.secondary:
return theme.colorScheme.secondary;
case ModernButtonType.outline:
case ModernButtonType.text:
return Colors.transparent;
case ModernButtonType.danger:
return theme.colorScheme.error;
}
}
Color _getTextColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.onSurfaceVariant;
}
switch (widget.type) {
case ModernButtonType.primary:
case ModernButtonType.secondary:
case ModernButtonType.danger:
return theme.colorScheme.onPrimary;
case ModernButtonType.outline:
return theme.colorScheme.primary;
case ModernButtonType.text:
return theme.colorScheme.primary;
}
}
Color _getBorderColor(ThemeData theme) {
if (!_canPress) {
return theme.colorScheme.outline;
}
switch (widget.type) {
case ModernButtonType.outline:
return theme.colorScheme.primary;
case ModernButtonType.danger:
return theme.colorScheme.error;
default:
return Colors.transparent;
}
}
double _getHeight() {
if (widget.height != null) return widget.height!;
switch (widget.size) {
case ModernButtonSize.small:
return UIConstants.buttonHeightSmall +
4; // extra room to avoid text clip
case ModernButtonSize.medium:
return UIConstants.buttonHeightMedium + 4;
case ModernButtonSize.large:
return UIConstants.buttonHeightLarge + 4;
}
}
EdgeInsetsGeometry _getPadding() {
if (widget.padding != null) return widget.padding!;
switch (widget.size) {
case ModernButtonSize.small:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing6,
);
case ModernButtonSize.medium:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing14,
);
case ModernButtonSize.large:
return const EdgeInsets.symmetric(
horizontal: UIConstants.spacing32,
vertical: UIConstants.spacing18,
);
}
}
double _getBorderRadius() {
if (widget.borderRadius != null) {
return widget.borderRadius!.topLeft.x;
}
switch (widget.size) {
case ModernButtonSize.small:
return UIConstants.radius8;
case ModernButtonSize.medium:
return UIConstants.radius16;
case ModernButtonSize.large:
return UIConstants.radius20;
}
}
TextStyle _getTextStyle(ThemeData theme) {
switch (widget.size) {
case ModernButtonSize.small:
return theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
case ModernButtonSize.medium:
return theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
case ModernButtonSize.large:
return theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
) ??
const TextStyle();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
width: widget.width,
height: _getHeight(),
decoration: BoxDecoration(
color: _getBackgroundColor(theme),
borderRadius: BorderRadius.circular(_getBorderRadius()),
border: Border.all(
color: _getBorderColor(theme),
width: widget.type == ModernButtonType.outline ? 1.5 : 0,
),
boxShadow: _canPress
? [
BoxShadow(
color: _getBackgroundColor(theme).withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _canPress ? widget.onPressed : null,
borderRadius: BorderRadius.circular(_getBorderRadius()),
child: Container(
padding: _getPadding(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.isLoading) ...[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
_getTextColor(theme),
),
),
),
if (!widget.isIconOnly) ...[
const SizedBox(width: UIConstants.spacing12),
],
] else ...[
if (widget.icon != null && !widget.isIconOnly) ...[
IconTheme(
data: IconThemeData(
color: _getTextColor(theme),
size: _getHeight() * 0.4,
),
child: widget.icon!,
),
const SizedBox(width: UIConstants.spacing12),
],
],
if (!widget.isIconOnly) ...[
Flexible(
child: Text(
widget.text,
style: _getTextStyle(theme).copyWith(
color: _getTextColor(theme),
height: 1.2,
),
strutStyle: const StrutStyle(
height: 1.2,
forceStrutHeight: true,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
class QuickActionButton extends StatefulWidget {
final String label;
final IconData icon;
final VoidCallback? onTap;
final Color? backgroundColor;
final Color? iconColor;
final Color? labelColor;
final double? size;
final bool isLoading;
final bool isDisabled;
const QuickActionButton({
super.key,
required this.label,
required this.icon,
this.onTap,
this.backgroundColor,
this.iconColor,
this.labelColor,
this.size,
this.isLoading = false,
this.isDisabled = false,
});
@override
State<QuickActionButton> createState() => _QuickActionButtonState();
}
class _QuickActionButtonState extends State<QuickActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.reverse();
}
}
void _onTapCancel() {
if (widget.onTap != null && !widget.isDisabled && !widget.isLoading) {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final buttonSize = widget.size ?? UIConstants.logoSizeMedium;
final backgroundColor =
widget.backgroundColor ?? colorScheme.primaryContainer;
final iconColor = widget.iconColor ?? colorScheme.onPrimaryContainer;
final labelColor = widget.labelColor ?? colorScheme.onSurface;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(UIConstants.radius16),
boxShadow: [
BoxShadow(
color: backgroundColor.withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: (widget.onTap != null &&
!widget.isDisabled &&
!widget.isLoading)
? widget.onTap
: null,
borderRadius: BorderRadius.circular(UIConstants.radius16),
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon
if (widget.isLoading)
SizedBox(
width: buttonSize * 0.3,
height: buttonSize * 0.3,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(iconColor),
),
)
else
Icon(
widget.icon,
color: iconColor,
size: buttonSize * 0.4,
),
const SizedBox(height: UIConstants.spacing8),
// Label
Text(
widget.label,
style: theme.textTheme.labelMedium?.copyWith(
color: labelColor,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,355 @@
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
enum DashboardCardType {
primary,
secondary,
success,
warning,
info,
danger,
}
class DashboardCard extends StatefulWidget {
final String title;
final String? subtitle;
final String? value;
final num? numericValue;
final String? valueSuffix;
final Duration? animationDuration;
final IconData? icon;
final DashboardCardType type;
final VoidCallback? onTap;
final Widget? trailing;
final bool isLoading;
final Color? customColor;
final double? elevation;
final BorderRadius? borderRadius;
const DashboardCard({
super.key,
required this.title,
this.subtitle,
this.value,
this.numericValue,
this.valueSuffix,
this.animationDuration,
this.icon,
this.type = DashboardCardType.primary,
this.onTap,
this.trailing,
this.isLoading = false,
this.customColor,
this.elevation,
this.borderRadius,
});
@override
State<DashboardCard> createState() => _DashboardCardState();
}
class _DashboardCardState extends State<DashboardCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _elevationAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_elevationAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = true;
});
_animationController.forward();
}
}
void _onTapUp(TapUpDetails details) {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
void _onTapCancel() {
if (widget.onTap != null && !widget.isLoading) {
setState(() {
_isPressed = false;
});
_animationController.reverse();
}
}
Color _getCardColor(ThemeData theme) {
if (widget.customColor != null) return widget.customColor!;
switch (widget.type) {
case DashboardCardType.primary:
return theme.colorScheme.primary;
case DashboardCardType.secondary:
return theme.colorScheme.secondary;
case DashboardCardType.success:
return theme.colorScheme.tertiary;
case DashboardCardType.warning:
return const Color(0xFFF59E0B);
case DashboardCardType.info:
return theme.colorScheme.primary;
case DashboardCardType.danger:
return theme.colorScheme.error;
}
}
Color _getIconColor(ThemeData theme) {
if (widget.customColor != null) return Colors.white;
switch (widget.type) {
case DashboardCardType.primary:
case DashboardCardType.secondary:
case DashboardCardType.success:
case DashboardCardType.warning:
case DashboardCardType.info:
case DashboardCardType.danger:
return Colors.white;
}
}
double _getElevation() {
if (widget.elevation != null) return widget.elevation!;
return UIConstants.elevation4;
}
BorderRadius _getBorderRadius() {
if (widget.borderRadius != null) return widget.borderRadius!;
return BorderRadius.circular(UIConstants.radius16);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final cardColor = _getCardColor(theme);
final iconColor = _getIconColor(theme);
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Container(
decoration: BoxDecoration(
borderRadius: _getBorderRadius(),
color: cardColor,
boxShadow: [
BoxShadow(
color: cardColor.withOpacity(0.3),
blurRadius: 8 * _elevationAnimation.value,
offset: Offset(0, 4 * _elevationAnimation.value),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap,
borderRadius: _getBorderRadius(),
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing16),
child: LayoutBuilder(
builder: (context, constraints) {
final isTight = constraints.maxHeight < 150;
final isVeryTight = constraints.maxHeight < 130;
final spacing16 = isTight
? UIConstants.spacing8
: UIConstants.spacing16;
final spacing12 = isTight
? UIConstants.spacing6
: UIConstants.spacing12;
final iconSize = isTight
? UIConstants.iconSizeMedium
: UIConstants.iconSizeLarge;
final titleStyle =
theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: isVeryTight ? 12 : (isTight ? 14 : null),
);
final subtitleStyle =
theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: isVeryTight ? 10 : (isTight ? 12 : null),
);
final valueStyle =
theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: isVeryTight ? 16 : (isTight ? 18 : null),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Icon
if (widget.icon != null)
Container(
padding: EdgeInsets.all(spacing12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(
UIConstants.radius12),
),
child: Icon(
widget.icon,
color: iconColor,
size: iconSize,
),
),
// Trailing Widget
if (widget.trailing != null) widget.trailing!,
],
),
SizedBox(height: spacing16),
// Title
Text(
widget.title,
style: titleStyle,
maxLines: isTight ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
// Subtitle
if (widget.subtitle != null && !isVeryTight) ...[
SizedBox(height: spacing12),
Text(
widget.subtitle!,
style: subtitleStyle,
maxLines: isTight ? 1 : 2,
overflow: TextOverflow.ellipsis,
),
],
// Value (animated if numeric)
SizedBox(height: spacing16),
if (widget.numericValue != null)
TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: widget.numericValue!.toDouble()),
duration: widget.animationDuration ??
UIConstants.durationNormal,
curve: UIConstants.curveNormal,
builder: (context, value, child) {
final text =
_formatNumber(value, widget.valueSuffix);
final valueText = Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: valueStyle,
);
return isTight
? FittedBox(
alignment: Alignment.centerLeft,
child: valueText)
: valueText;
},
)
else if (widget.value != null)
Builder(builder: (context) {
final valueText = Text(
widget.value!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: valueStyle,
);
return isTight
? FittedBox(
alignment: Alignment.centerLeft,
child: valueText)
: valueText;
}),
// Loading Indicator
if (widget.isLoading) ...[
SizedBox(height: spacing16),
const LinearProgressIndicator(
backgroundColor: Colors.white24,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
],
],
);
},
),
),
),
),
),
),
);
},
);
}
String _formatNumber(double value, String? suffix) {
String formatted;
if (value >= 1000000) {
formatted = "${(value / 1000000).toStringAsFixed(1)}M";
} else if (value >= 1000) {
formatted = "${(value / 1000).toStringAsFixed(1)}K";
} else {
if (value == value.roundToDouble()) {
formatted = value.toInt().toString();
} else {
formatted = value.toStringAsFixed(1);
}
}
if (suffix != null && suffix.isNotEmpty) {
return "$formatted$suffix";
}
return formatted;
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/ui_constants.dart';
class SystemParameterSection extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final List<Widget> children;
final bool isExpanded;
final VoidCallback? onToggle;
final Color? iconColor;
const SystemParameterSection({
super.key,
required this.title,
this.subtitle,
required this.icon,
required this.children,
this.isExpanded = true,
this.onToggle,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(UIConstants.radius16),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(
color: colorScheme.outline.withOpacity(0.1),
width: 1,
),
),
child: Column(
children: [
// Section Header
InkWell(
onTap: onToggle,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(UIConstants.radius16),
),
child: Container(
padding: const EdgeInsets.all(UIConstants.spacing20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primaryContainer.withOpacity(0.3),
colorScheme.secondaryContainer.withOpacity(0.1),
],
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(UIConstants.radius16),
),
),
child: Row(
children: [
// Icon
Container(
padding: const EdgeInsets.all(UIConstants.spacing12),
decoration: BoxDecoration(
color: iconColor?.withOpacity(0.1) ??
colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
child: Icon(
icon,
color: iconColor ?? colorScheme.primary,
size: UIConstants.iconSizeMedium,
),
),
const SizedBox(width: UIConstants.spacing16),
// Title and Subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
if (subtitle != null) ...[
const SizedBox(height: UIConstants.spacing4),
Text(
subtitle!,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
],
),
),
// Expand/Collapse Icon
if (onToggle != null)
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: UIConstants.durationFast,
child: Icon(
Icons.keyboard_arrow_down,
color: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
),
],
),
),
),
// Section Content
AnimatedContainer(
duration: UIConstants.durationFast,
curve: UIConstants.curveNormal,
height: isExpanded ? null : 0,
child: isExpanded
? Container(
padding: const EdgeInsets.all(UIConstants.spacing20),
child: Column(
children: children,
),
)
: const SizedBox.shrink(),
),
],
),
);
}
}

View File

@@ -0,0 +1,357 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../../../core/constants/ui_constants.dart';
import '../buttons/modern_button.dart';
class ModernImagePicker extends StatefulWidget {
final String? label;
final String? hint;
final Uint8List? imageBytes;
final File? imageFile;
final String? imageUrl;
final VoidCallback? onPickImage;
final VoidCallback? onRemoveImage;
final bool isLoading;
final double? height;
final double? width;
final BorderRadius? borderRadius;
final String? errorText;
final bool showRemoveButton;
final IconData? placeholderIcon;
final String? placeholderText;
const ModernImagePicker({
super.key,
this.label,
this.hint,
this.imageBytes,
this.imageFile,
this.imageUrl,
this.onPickImage,
this.onRemoveImage,
this.isLoading = false,
this.height = 200,
this.width,
this.borderRadius,
this.errorText,
this.showRemoveButton = true,
this.placeholderIcon,
this.placeholderText,
});
@override
State<ModernImagePicker> createState() => _ModernImagePickerState();
}
class _ModernImagePickerState extends State<ModernImagePicker>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
_fadeAnimation = Tween<double>(
begin: 1.0,
end: 0.8,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
bool get hasImage =>
widget.imageBytes != null ||
widget.imageFile != null ||
(widget.imageUrl != null && widget.imageUrl!.isNotEmpty);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label
if (widget.label != null) ...[
Text(
widget.label!,
style: theme.textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing12),
],
// Image Container
GestureDetector(
onTapDown: (_) => _animationController.forward(),
onTapUp: (_) => _animationController.reverse(),
onTapCancel: () => _animationController.reverse(),
onTap: widget.onPickImage,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
height: widget.height,
width: widget.width ?? double.infinity,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ??
BorderRadius.circular(UIConstants.radius16),
border: Border.all(
color: widget.errorText != null
? colorScheme.error
: colorScheme.outline.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: widget.borderRadius ??
BorderRadius.circular(UIConstants.radius16),
child: Stack(
children: [
// Image or Placeholder
if (hasImage)
_buildImageWidget()
else
_buildPlaceholder(theme, colorScheme),
// Loading Overlay
if (widget.isLoading)
_buildLoadingOverlay(colorScheme),
// Remove Button
if (hasImage &&
widget.showRemoveButton &&
widget.onRemoveImage != null)
_buildRemoveButton(colorScheme),
],
),
),
),
),
);
},
),
),
// Hint Text
if (widget.hint != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
widget.hint!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
// Error Text
if (widget.errorText != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
widget.errorText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error,
),
),
],
// Action Buttons
const SizedBox(height: UIConstants.spacing16),
Row(
children: [
Expanded(
child: ModernButton(
text: hasImage ? 'Change Image' : 'Pick Image',
type: ModernButtonType.outline,
size: ModernButtonSize.medium,
icon: Icon(hasImage ? Icons.edit : Icons.add_photo_alternate),
onPressed: widget.isLoading ? null : widget.onPickImage,
isLoading: widget.isLoading,
),
),
if (hasImage && widget.onRemoveImage != null) ...[
const SizedBox(width: UIConstants.spacing12),
ModernButton(
text: 'Remove',
type: ModernButtonType.danger,
size: ModernButtonSize.medium,
icon: Icon(Icons.delete_outline),
onPressed: widget.isLoading ? null : widget.onRemoveImage,
),
],
],
),
],
);
}
Widget _buildImageWidget() {
if (widget.imageBytes != null) {
return Image.memory(
widget.imageBytes!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
} else if (widget.imageFile != null) {
return Image.file(
widget.imageFile!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
);
} else if (widget.imageUrl != null && widget.imageUrl!.isNotEmpty) {
return Image.network(
widget.imageUrl!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholder(
Theme.of(context), Theme.of(context).colorScheme);
},
);
}
return const SizedBox.shrink();
}
Widget _buildPlaceholder(ThemeData theme, ColorScheme colorScheme) {
return Container(
width: double.infinity,
height: double.infinity,
color: colorScheme.surfaceVariant.withOpacity(0.3),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.placeholderIcon ?? Icons.add_photo_alternate,
size: UIConstants.iconSizeXLarge,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: UIConstants.spacing12),
Text(
widget.placeholderText ?? 'Tap to select image',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: UIConstants.spacing8),
Text(
'JPG, PNG, GIF up to 10MB',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildLoadingOverlay(ColorScheme colorScheme) {
return Container(
width: double.infinity,
height: double.infinity,
color: colorScheme.surface.withOpacity(0.8),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: colorScheme.primary,
),
const SizedBox(height: UIConstants.spacing12),
Text(
'Uploading...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface,
),
),
],
),
),
);
}
Widget _buildRemoveButton(ColorScheme colorScheme) {
return Positioned(
top: UIConstants.spacing8,
right: UIConstants.spacing8,
child: Container(
decoration: BoxDecoration(
color: colorScheme.error,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: widget.onRemoveImage,
icon: Icon(
Icons.close,
color: colorScheme.onError,
size: UIConstants.iconSizeSmall,
),
padding: const EdgeInsets.all(UIConstants.spacing4),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
),
);
}
}

View File

@@ -0,0 +1,298 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../../core/constants/ui_constants.dart';
class ModernTextField extends StatefulWidget {
final String? label;
final String? hint;
final String? helperText;
final String? errorText;
final TextEditingController? controller;
final TextInputType? keyboardType;
final bool obscureText;
final bool enabled;
final bool readOnly;
final int? maxLines;
final int? maxLength;
final Widget? prefixIcon;
final Widget? suffixIcon;
final VoidCallback? onSuffixIconPressed;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final void Function(String)? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final VoidCallback? onTap;
final bool autofocus;
final TextCapitalization textCapitalization;
final TextInputAction? textInputAction;
final bool expands;
final double? height;
final EdgeInsetsGeometry? contentPadding;
const ModernTextField({
super.key,
this.label,
this.hint,
this.helperText,
this.errorText,
this.controller,
this.keyboardType,
this.obscureText = false,
this.enabled = true,
this.readOnly = false,
this.maxLines = 1,
this.maxLength,
this.prefixIcon,
this.suffixIcon,
this.onSuffixIconPressed,
this.validator,
this.onChanged,
this.onSubmitted,
this.inputFormatters,
this.focusNode,
this.onTap,
this.autofocus = false,
this.textCapitalization = TextCapitalization.none,
this.textInputAction,
this.expands = false,
this.height,
this.contentPadding,
});
@override
State<ModernTextField> createState() => _ModernTextFieldState();
}
class _ModernTextFieldState extends State<ModernTextField>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
bool _isFocused = false;
bool _hasError = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationFast,
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
_scaleAnimation = Tween<double>(
begin: 0.95,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveFast,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onFocusChange(bool hasFocus) {
setState(() {
_isFocused = hasFocus;
});
if (hasFocus) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
void _onChanged(String value) {
setState(() {
_hasError = false;
});
if (widget.onChanged != null) {
widget.onChanged!(value);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.label != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
child: Text(
widget.label!,
style: theme.textTheme.labelLarge?.copyWith(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
],
Focus(
onFocusChange: _onFocusChange,
child: Container(
height: widget.height ?? UIConstants.inputHeightMedium,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(UIConstants.radius16),
color: _isFocused
? colorScheme.surfaceVariant.withOpacity(0.5)
: colorScheme.surfaceVariant.withOpacity(0.3),
border: Border.all(
color: _hasError
? colorScheme.error
: _isFocused
? colorScheme.primary
: colorScheme.outline.withOpacity(0.3),
width: _isFocused ? 2.0 : 1.5,
),
boxShadow: _isFocused
? [
BoxShadow(
color: colorScheme.primary.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: TextFormField(
controller: widget.controller,
focusNode: widget.focusNode,
keyboardType: widget.keyboardType,
obscureText: widget.obscureText,
enabled: widget.enabled,
readOnly: widget.readOnly,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
autofocus: widget.autofocus,
textCapitalization: widget.textCapitalization,
textInputAction: widget.textInputAction,
expands: widget.expands,
inputFormatters: widget.inputFormatters,
validator: (value) {
if (widget.validator != null) {
final result = widget.validator!(value);
setState(() {
_hasError = result != null;
});
return result;
}
return null;
},
onChanged: _onChanged,
onFieldSubmitted: widget.onSubmitted,
onTap: widget.onTap,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: widget.hint,
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(0.6),
),
prefixIcon: widget.prefixIcon != null
? Padding(
padding: const EdgeInsets.only(
left: UIConstants.spacing16),
child: IconTheme(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
child: widget.prefixIcon!,
),
)
: null,
suffixIcon: widget.suffixIcon != null
? Padding(
padding: const EdgeInsets.only(
right: UIConstants.spacing16),
child: IconTheme(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
child: widget.suffixIcon!,
),
)
: null,
border: InputBorder.none,
contentPadding: widget.contentPadding ??
const EdgeInsets.symmetric(
horizontal: UIConstants.spacing20,
vertical: UIConstants.spacing16,
),
counterText: '',
),
),
),
),
if (widget.helperText != null && !_hasError) ...[
Padding(
padding: const EdgeInsets.only(top: UIConstants.spacing8),
child: Text(
widget.helperText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
if (widget.errorText != null && _hasError) ...[
Padding(
padding: const EdgeInsets.only(top: UIConstants.spacing8),
child: Row(
children: [
Icon(
Icons.error_outline,
size: UIConstants.iconSizeSmall,
color: colorScheme.error,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
widget.errorText!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error,
),
),
),
],
),
),
],
],
),
);
},
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/ui_constants.dart';
import '../../../utils/managers/user_manager.dart';
import '../../../view_model/profile/profile_view_model.dart';
import '../buttons/modern_button.dart';
class ModernDrawer extends StatelessWidget {
final List<DrawerItem> items;
final Widget? header;
final Widget? footer;
final Color? backgroundColor;
final double? elevation;
const ModernDrawer({
super.key,
required this.items,
this.header,
this.footer,
this.backgroundColor,
this.elevation,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final email = UserManager().email;
final userName = UserManager().userName;
return Drawer(
backgroundColor: backgroundColor ?? colorScheme.surface,
elevation: elevation ?? UIConstants.elevation8,
child: Column(
children: [
// Header
header ??
_buildDefaultHeader(context, theme, colorScheme, userName, email),
// Navigation Items
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return _buildDrawerItem(context, theme, colorScheme, item);
},
),
),
// Footer
if (footer != null) footer!,
// Logout Section
_buildLogoutSection(context, theme, colorScheme),
],
),
);
}
Widget _buildDefaultHeader(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
String? userName,
String? email,
) {
return Consumer<ProfileViewModel>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.all(UIConstants.spacing24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.secondary,
colorScheme.tertiary,
],
),
),
child: SafeArea(
child: Column(
children: [
// Profile Picture
Container(
width: UIConstants.logoSizeLarge,
height: UIConstants.logoSizeLarge,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 3,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: CircleAvatar(
radius: UIConstants.logoSizeLarge / 2,
backgroundColor: Colors.white.withOpacity(0.2),
backgroundImage: provider.profileImageBytes != null
? MemoryImage(provider.profileImageBytes!)
: null,
child: provider.profileImageBytes != null
? null
: Icon(
Icons.person,
size: UIConstants.iconSizeLarge,
color: Colors.white,
),
),
),
const SizedBox(height: UIConstants.spacing16),
// User Name
Text(
"Hello, ${userName ?? 'User'}",
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
// User Email
if (email != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
email,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
],
),
),
);
},
);
}
Widget _buildDrawerItem(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
DrawerItem item,
) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing4,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(UIConstants.radius12),
color: item.isSelected
? colorScheme.primaryContainer.withOpacity(0.3)
: Colors.transparent,
),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(UIConstants.spacing8),
decoration: BoxDecoration(
color: item.isSelected
? colorScheme.primaryContainer
: colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius8),
),
child: Icon(
item.icon,
color: item.isSelected
? colorScheme.onPrimaryContainer
: item.color ?? colorScheme.onSurfaceVariant,
size: UIConstants.iconSizeMedium,
),
),
title: Text(
item.title,
style: theme.textTheme.titleMedium?.copyWith(
color:
item.isSelected ? colorScheme.onSurface : colorScheme.onSurface,
fontWeight: item.isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
subtitle: item.subtitle != null
? Text(
item.subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
)
: null,
trailing: item.trailing,
onTap: () => item.onTap(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UIConstants.radius12),
),
),
);
}
Widget _buildLogoutSection(
BuildContext context,
ThemeData theme,
ColorScheme colorScheme,
) {
return Container(
padding: const EdgeInsets.all(UIConstants.spacing24),
child: ModernButton(
text: 'Logout',
type: ModernButtonType.danger,
size: ModernButtonSize.medium,
icon: Icon(Icons.logout),
onPressed: () async {
await UserManager().clearUser();
if (context.mounted) {
Navigator.pushReplacementNamed(context, '/splash');
}
},
),
);
}
}
class DrawerItem {
final IconData icon;
final String title;
final String? subtitle;
final void Function(BuildContext) onTap;
final Color? color;
final bool isSelected;
final Widget? trailing;
const DrawerItem({
required this.icon,
required this.title,
this.subtitle,
required this.onTap,
this.color,
this.isSelected = false,
this.trailing,
});
}

View File

@@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/constants/ui_constants.dart';
import '../../core/providers/dynamic_theme_provider.dart';
class ThemePreview extends StatelessWidget {
const ThemePreview({super.key});
@override
Widget build(BuildContext context) {
return Consumer<DynamicThemeProvider>(
builder: (context, dynamicThemeProvider, child) {
if (!dynamicThemeProvider.isUsingDynamicTheme) {
return const SizedBox.shrink();
}
final colorScheme = dynamicThemeProvider.dynamicColorScheme;
final logoColors = dynamicThemeProvider.logoColors;
final paletteInfo = dynamicThemeProvider.getColorPaletteInfo();
if (colorScheme == null || logoColors.isEmpty) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.all(UIConstants.spacing16),
padding: const EdgeInsets.all(UIConstants.spacing20),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(UIConstants.radius16),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(
Icons.palette,
color: colorScheme.primary,
size: UIConstants.iconSizeLarge,
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dynamic Theme Generated',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
Text(
dynamicThemeProvider.getThemeDescription(),
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
IconButton(
onPressed: () => dynamicThemeProvider.resetToDefaultTheme(),
icon: Icon(
Icons.refresh,
color: colorScheme.primary,
),
tooltip: 'Reset to Default Theme',
),
],
),
const SizedBox(height: UIConstants.spacing20),
// Color Palette
Text(
'Color Palette',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing12),
// Color Swatches
Wrap(
spacing: UIConstants.spacing12,
runSpacing: UIConstants.spacing12,
children: logoColors.take(5).map((color) {
final index = logoColors.indexOf(color);
final labels = [
'Primary',
'Secondary',
'Tertiary',
'Accent 1',
'Accent 2'
];
return Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Icon(
Icons.check,
color: _getContrastColor(color),
size: 24,
),
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
index < labels.length
? labels[index]
: 'Color ${index + 1}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
);
}).toList(),
),
const SizedBox(height: UIConstants.spacing20),
// Theme Info
Container(
padding: const EdgeInsets.all(UIConstants.spacing16),
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(UIConstants.radius12),
border: Border.all(
color: colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Theme Information',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: UIConstants.spacing8),
_buildInfoRow(
'Total Colors', '${paletteInfo['totalColors']}'),
_buildInfoRow('Description', paletteInfo['description']),
if (paletteInfo['characteristics'] != null) ...[
_buildInfoRow(
'Warm Colors',
paletteInfo['characteristics']['warm']
? 'Yes'
: 'No'),
_buildInfoRow(
'Cool Colors',
paletteInfo['characteristics']['cool']
? 'Yes'
: 'No'),
_buildInfoRow(
'Neutral Colors',
paletteInfo['characteristics']['neutral']
? 'Yes'
: 'No'),
],
],
),
),
const SizedBox(height: UIConstants.spacing16),
// Preview Buttons
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UIConstants.radius12),
),
),
child: const Text('Primary Button'),
),
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.primary,
side: BorderSide(color: colorScheme.outline),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(UIConstants.radius12),
),
),
child: const Text('Secondary Button'),
),
),
],
),
],
),
);
},
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
);
}
Color _getContrastColor(Color backgroundColor) {
final luminance = backgroundColor.computeLuminance();
return luminance > 0.5 ? Colors.black : Colors.white;
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import '../../core/constants/ui_constants.dart';
class ThemeToggle extends StatefulWidget {
final bool isDarkMode;
final ValueChanged<bool> onThemeChanged;
final double size;
final Color? backgroundColor;
final Color? iconColor;
const ThemeToggle({
super.key,
required this.isDarkMode,
required this.onThemeChanged,
this.size = 48.0,
this.backgroundColor,
this.iconColor,
});
@override
State<ThemeToggle> createState() => _ThemeToggleState();
}
class _ThemeToggleState extends State<ThemeToggle>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: UIConstants.durationNormal,
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.5,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.8,
).animate(CurvedAnimation(
parent: _animationController,
curve: UIConstants.curveNormal,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleTheme() {
_animationController.forward().then((_) {
widget.onThemeChanged(!widget.isDarkMode);
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.backgroundColor ??
(widget.isDarkMode
? colorScheme.primaryContainer
: colorScheme.surfaceVariant),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _toggleTheme,
borderRadius: BorderRadius.circular(widget.size / 2),
child: Center(
child: Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159,
child: Icon(
widget.isDarkMode ? Icons.light_mode : Icons.dark_mode,
size: widget.size * 0.5,
color: widget.iconColor ??
(widget.isDarkMode
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant),
),
),
),
),
),
),
);
},
);
}
}