baseproject
This commit is contained in:
221
base_project/lib/shared/widgets/app_bar/modern_app_bar.dart
Normal file
221
base_project/lib/shared/widgets/app_bar/modern_app_bar.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
342
base_project/lib/shared/widgets/buttons/modern_button.dart
Normal file
342
base_project/lib/shared/widgets/buttons/modern_button.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
176
base_project/lib/shared/widgets/buttons/quick_action_button.dart
Normal file
176
base_project/lib/shared/widgets/buttons/quick_action_button.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
355
base_project/lib/shared/widgets/cards/dashboard_card.dart
Normal file
355
base_project/lib/shared/widgets/cards/dashboard_card.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
357
base_project/lib/shared/widgets/inputs/modern_image_picker.dart
Normal file
357
base_project/lib/shared/widgets/inputs/modern_image_picker.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
298
base_project/lib/shared/widgets/inputs/modern_text_field.dart
Normal file
298
base_project/lib/shared/widgets/inputs/modern_text_field.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
252
base_project/lib/shared/widgets/navigation/modern_drawer.dart
Normal file
252
base_project/lib/shared/widgets/navigation/modern_drawer.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
275
base_project/lib/shared/widgets/theme_preview.dart
Normal file
275
base_project/lib/shared/widgets/theme_preview.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
120
base_project/lib/shared/widgets/theme_toggle.dart
Normal file
120
base_project/lib/shared/widgets/theme_toggle.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user