1008 lines
33 KiB
Dart
1008 lines
33 KiB
Dart
|
|
import 'dart:math' as math;
|
||
|
|
|
||
|
|
import 'package:base_project/core/constants/ui_constants.dart';
|
||
|
|
|
||
|
|
import 'package:base_project/routes/route_names.dart';
|
||
|
|
|
||
|
|
import 'package:base_project/view_model/auth/auth_view_model.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:provider/provider.dart';
|
||
|
|
|
||
|
|
import '../../core/providers/dynamic_theme_provider.dart';
|
||
|
|
import '../../core/providers/theme_provider.dart';
|
||
|
|
import '../../shared/widgets/buttons/modern_button.dart';
|
||
|
|
import '../../shared/widgets/inputs/modern_text_field.dart';
|
||
|
|
import '../../shared/widgets/theme_toggle.dart';
|
||
|
|
|
||
|
|
class LoginView extends StatefulWidget {
|
||
|
|
const LoginView({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
_LoginViewState createState() => _LoginViewState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _LoginViewState extends State<LoginView> with TickerProviderStateMixin {
|
||
|
|
// Controllers
|
||
|
|
final TextEditingController _emailController = TextEditingController();
|
||
|
|
final TextEditingController _passwordController = TextEditingController();
|
||
|
|
final FocusNode _emailFocusNode = FocusNode();
|
||
|
|
final FocusNode _passwordFocusNode = FocusNode();
|
||
|
|
|
||
|
|
// Animation Controllers
|
||
|
|
late AnimationController _pageAnimationController;
|
||
|
|
late AnimationController _formAnimationController;
|
||
|
|
late AnimationController _logoAnimationController;
|
||
|
|
late AnimationController _backgroundAnimationController;
|
||
|
|
late AnimationController _particleAnimationController;
|
||
|
|
|
||
|
|
// Animations
|
||
|
|
late Animation<double> _pageFadeAnimation;
|
||
|
|
late Animation<Offset> _pageSlideAnimation;
|
||
|
|
late Animation<double> _formScaleAnimation;
|
||
|
|
late Animation<double> _logoScaleAnimation;
|
||
|
|
late Animation<double> _logoRotationAnimation;
|
||
|
|
late Animation<double> _backgroundOpacityAnimation;
|
||
|
|
late Animation<double> _particleRotationAnimation;
|
||
|
|
|
||
|
|
// State
|
||
|
|
bool _isPasswordVisible = false;
|
||
|
|
bool _isFormValid = false;
|
||
|
|
bool _isEmailFocused = false;
|
||
|
|
bool _isPasswordFocused = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_initializeAnimations();
|
||
|
|
_startPageAnimation();
|
||
|
|
_setupFocusListeners();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _setupFocusListeners() {
|
||
|
|
_emailFocusNode.addListener(() {
|
||
|
|
setState(() {
|
||
|
|
_isEmailFocused = _emailFocusNode.hasFocus;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
_passwordFocusNode.addListener(() {
|
||
|
|
setState(() {
|
||
|
|
_isPasswordFocused = _passwordFocusNode.hasFocus;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _initializeAnimations() {
|
||
|
|
// Page Animation Controller
|
||
|
|
_pageAnimationController = AnimationController(
|
||
|
|
duration: UIConstants.durationSlow,
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Form Animation Controller
|
||
|
|
_formAnimationController = AnimationController(
|
||
|
|
duration: UIConstants.durationNormal,
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Logo Animation Controller
|
||
|
|
_logoAnimationController = AnimationController(
|
||
|
|
duration: UIConstants.durationVerySlow,
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Background Animation Controller
|
||
|
|
_backgroundAnimationController = AnimationController(
|
||
|
|
duration: UIConstants.durationSlow,
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Particle Animation Controller
|
||
|
|
_particleAnimationController = AnimationController(
|
||
|
|
duration: const Duration(seconds: 10),
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Page Animations
|
||
|
|
_pageFadeAnimation = Tween<double>(
|
||
|
|
begin: 0.0,
|
||
|
|
end: 1.0,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _pageAnimationController,
|
||
|
|
curve: UIConstants.curveNormal,
|
||
|
|
));
|
||
|
|
|
||
|
|
_pageSlideAnimation = Tween<Offset>(
|
||
|
|
begin: const Offset(0, 0.3),
|
||
|
|
end: Offset.zero,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _pageAnimationController,
|
||
|
|
curve: UIConstants.curveNormal,
|
||
|
|
));
|
||
|
|
|
||
|
|
// Form Animations
|
||
|
|
_formScaleAnimation = Tween<double>(
|
||
|
|
begin: 0.8,
|
||
|
|
end: 1.0,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _formAnimationController,
|
||
|
|
curve: UIConstants.curveElastic,
|
||
|
|
));
|
||
|
|
|
||
|
|
// Logo Animations
|
||
|
|
_logoScaleAnimation = Tween<double>(
|
||
|
|
begin: 0.0,
|
||
|
|
end: 1.0,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _logoAnimationController,
|
||
|
|
curve: UIConstants.curveElastic,
|
||
|
|
));
|
||
|
|
|
||
|
|
_logoRotationAnimation = Tween<double>(
|
||
|
|
begin: -0.5,
|
||
|
|
end: 0.0,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _logoAnimationController,
|
||
|
|
curve: UIConstants.curveElastic,
|
||
|
|
));
|
||
|
|
|
||
|
|
// Background Animations
|
||
|
|
_backgroundOpacityAnimation = Tween<double>(
|
||
|
|
begin: 0.0,
|
||
|
|
end: 1.0,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _backgroundAnimationController,
|
||
|
|
curve: UIConstants.curveNormal,
|
||
|
|
));
|
||
|
|
|
||
|
|
// Particle Animations
|
||
|
|
_particleRotationAnimation = Tween<double>(
|
||
|
|
begin: 0.0,
|
||
|
|
end: 2 * math.pi,
|
||
|
|
).animate(CurvedAnimation(
|
||
|
|
parent: _particleAnimationController,
|
||
|
|
curve: Curves.linear,
|
||
|
|
));
|
||
|
|
|
||
|
|
// Listen to form changes
|
||
|
|
_emailController.addListener(_validateForm);
|
||
|
|
_passwordController.addListener(_validateForm);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _startPageAnimation() async {
|
||
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
||
|
|
_backgroundAnimationController.forward();
|
||
|
|
|
||
|
|
await Future.delayed(const Duration(milliseconds: 200));
|
||
|
|
_pageAnimationController.forward();
|
||
|
|
|
||
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
||
|
|
_logoAnimationController.forward();
|
||
|
|
|
||
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
||
|
|
_formAnimationController.forward();
|
||
|
|
|
||
|
|
_particleAnimationController.repeat();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _validateForm() {
|
||
|
|
final isValid =
|
||
|
|
_emailController.text.isNotEmpty && _passwordController.text.isNotEmpty;
|
||
|
|
|
||
|
|
if (isValid != _isFormValid) {
|
||
|
|
setState(() {
|
||
|
|
_isFormValid = isValid;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_emailController.dispose();
|
||
|
|
_passwordController.dispose();
|
||
|
|
_emailFocusNode.dispose();
|
||
|
|
_passwordFocusNode.dispose();
|
||
|
|
_pageAnimationController.dispose();
|
||
|
|
_formAnimationController.dispose();
|
||
|
|
_logoAnimationController.dispose();
|
||
|
|
_backgroundAnimationController.dispose();
|
||
|
|
_particleAnimationController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
// Always use dynamic theme provider for guaranteed dynamic colors
|
||
|
|
return Consumer<DynamicThemeProvider>(
|
||
|
|
builder: (context, dynamicThemeProvider, child) {
|
||
|
|
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||
|
|
Theme.of(context).brightness == Brightness.dark);
|
||
|
|
|
||
|
|
final size = MediaQuery.of(context).size;
|
||
|
|
|
||
|
|
return Scaffold(
|
||
|
|
body: Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topLeft,
|
||
|
|
end: Alignment.bottomRight,
|
||
|
|
colors: [
|
||
|
|
colorScheme.primary,
|
||
|
|
colorScheme.primaryContainer,
|
||
|
|
colorScheme.secondary,
|
||
|
|
colorScheme.tertiary,
|
||
|
|
],
|
||
|
|
stops: const [0.0, 0.3, 0.7, 1.0],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Stack(
|
||
|
|
children: [
|
||
|
|
// Animated Background Particles
|
||
|
|
_buildParticleBackground(colorScheme),
|
||
|
|
|
||
|
|
// Glassmorphism Background
|
||
|
|
_buildGlassmorphismBackground(colorScheme),
|
||
|
|
|
||
|
|
// Theme Toggle Button
|
||
|
|
Positioned(
|
||
|
|
top: UIConstants.spacing16,
|
||
|
|
right: UIConstants.spacing16,
|
||
|
|
child: Consumer<ThemeProvider>(
|
||
|
|
builder: (context, themeProvider, child) {
|
||
|
|
return Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: colorScheme.surface.withOpacity(0.1),
|
||
|
|
borderRadius:
|
||
|
|
BorderRadius.circular(UIConstants.radiusFull),
|
||
|
|
border: Border.all(
|
||
|
|
color: colorScheme.surface.withOpacity(0.2),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: ThemeToggle(
|
||
|
|
isDarkMode: themeProvider.isDarkMode,
|
||
|
|
onThemeChanged: (isDark) {
|
||
|
|
themeProvider.setTheme(isDark);
|
||
|
|
},
|
||
|
|
size: UIConstants.iconSizeLarge,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// Main Content
|
||
|
|
SafeArea(
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
padding: UIConstants.getResponsivePadding(
|
||
|
|
context,
|
||
|
|
mobile: const EdgeInsets.all(UIConstants.spacing16),
|
||
|
|
tablet: const EdgeInsets.all(UIConstants.spacing24),
|
||
|
|
desktop: const EdgeInsets.all(UIConstants.spacing32),
|
||
|
|
),
|
||
|
|
child: ConstrainedBox(
|
||
|
|
constraints: BoxConstraints(
|
||
|
|
minHeight: size.height -
|
||
|
|
MediaQuery.of(context).padding.top -
|
||
|
|
MediaQuery.of(context).padding.bottom,
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
// Logo Section
|
||
|
|
_buildLogoSection(colorScheme),
|
||
|
|
|
||
|
|
SizedBox(
|
||
|
|
height: UIConstants.getResponsiveSpacing(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.spacing32,
|
||
|
|
tablet: UIConstants.spacing40,
|
||
|
|
desktop: UIConstants.spacing48,
|
||
|
|
)),
|
||
|
|
|
||
|
|
// Welcome Text
|
||
|
|
_buildWelcomeSection(colorScheme),
|
||
|
|
|
||
|
|
SizedBox(
|
||
|
|
height: UIConstants.getResponsiveSpacing(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.spacing32,
|
||
|
|
tablet: UIConstants.spacing40,
|
||
|
|
desktop: UIConstants.spacing48,
|
||
|
|
)),
|
||
|
|
|
||
|
|
// Login Form
|
||
|
|
_buildLoginForm(colorScheme),
|
||
|
|
|
||
|
|
SizedBox(
|
||
|
|
height: UIConstants.getResponsiveSpacing(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.spacing20,
|
||
|
|
tablet: UIConstants.spacing24,
|
||
|
|
desktop: UIConstants.spacing32,
|
||
|
|
)),
|
||
|
|
|
||
|
|
// Login Button
|
||
|
|
_buildLoginButton(colorScheme),
|
||
|
|
|
||
|
|
SizedBox(
|
||
|
|
height: UIConstants.getResponsiveSpacing(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.spacing32,
|
||
|
|
tablet: UIConstants.spacing40,
|
||
|
|
desktop: UIConstants.spacing48,
|
||
|
|
)),
|
||
|
|
|
||
|
|
// Sign Up Section
|
||
|
|
_buildSignUpSection(colorScheme),
|
||
|
|
|
||
|
|
SizedBox(
|
||
|
|
height: UIConstants.getResponsiveSpacing(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.spacing20,
|
||
|
|
tablet: UIConstants.spacing24,
|
||
|
|
desktop: UIConstants.spacing32,
|
||
|
|
)),
|
||
|
|
|
||
|
|
// Social Login Section
|
||
|
|
_buildSocialLoginSection(colorScheme),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildParticleBackground(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _particleRotationAnimation,
|
||
|
|
builder: (context, child) {
|
||
|
|
return CustomPaint(
|
||
|
|
painter: ParticlePainter(
|
||
|
|
rotation: _particleRotationAnimation.value,
|
||
|
|
color: colorScheme.surface.withOpacity(0.1),
|
||
|
|
),
|
||
|
|
size: Size.infinite,
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildGlassmorphismBackground(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _backgroundAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return FadeTransition(
|
||
|
|
opacity: _backgroundOpacityAnimation,
|
||
|
|
child: Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topLeft,
|
||
|
|
end: Alignment.bottomRight,
|
||
|
|
colors: [
|
||
|
|
colorScheme.surface.withOpacity(0.05),
|
||
|
|
colorScheme.surface.withOpacity(0.1),
|
||
|
|
colorScheme.surface.withOpacity(0.05),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildLogoSection(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _logoAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return Transform.scale(
|
||
|
|
scale: _logoScaleAnimation.value,
|
||
|
|
child: Transform.rotate(
|
||
|
|
angle: _logoRotationAnimation.value,
|
||
|
|
child: Container(
|
||
|
|
width: UIConstants.getResponsiveValue(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.logoSizeLarge, // Reduced from 1.5x
|
||
|
|
tablet: UIConstants.logoSizeXLarge, // Reduced from 1.5x
|
||
|
|
desktop: UIConstants.logoSizeXLarge, // Reduced from 1.5x
|
||
|
|
),
|
||
|
|
height: UIConstants.getResponsiveValue(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.logoSizeLarge, // Reduced from 1.5x
|
||
|
|
tablet: UIConstants.logoSizeXLarge, // Reduced from 1.5x
|
||
|
|
desktop: UIConstants.logoSizeXLarge, // Reduced from 1.5x
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topLeft,
|
||
|
|
end: Alignment.bottomRight,
|
||
|
|
colors: [
|
||
|
|
colorScheme.surface.withOpacity(0.2),
|
||
|
|
colorScheme.surface.withOpacity(0.1),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
border: Border.all(
|
||
|
|
color: colorScheme.surface.withOpacity(0.3),
|
||
|
|
width: 2,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: colorScheme.primary.withOpacity(0.3),
|
||
|
|
blurRadius: 30,
|
||
|
|
offset: const Offset(0, 15),
|
||
|
|
spreadRadius: 5,
|
||
|
|
),
|
||
|
|
BoxShadow(
|
||
|
|
color: colorScheme.secondary.withOpacity(0.2),
|
||
|
|
blurRadius: 50,
|
||
|
|
offset: const Offset(0, 25),
|
||
|
|
spreadRadius: 10,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Center(
|
||
|
|
child: Icon(
|
||
|
|
Icons.security,
|
||
|
|
size: UIConstants.getResponsiveValue(
|
||
|
|
context,
|
||
|
|
mobile: UIConstants.iconSizeLarge,
|
||
|
|
tablet: UIConstants.iconSizeXLarge,
|
||
|
|
desktop: UIConstants.iconSizeXLarge,
|
||
|
|
),
|
||
|
|
color: colorScheme.onSurface,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildWelcomeSection(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _pageAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return FadeTransition(
|
||
|
|
opacity: _pageFadeAnimation,
|
||
|
|
child: SlideTransition(
|
||
|
|
position: _pageSlideAnimation,
|
||
|
|
child: Container(
|
||
|
|
padding: UIConstants.cardPaddingMedium,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.95),
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||
|
|
border: Border.all(
|
||
|
|
color: Colors.white.withOpacity(0.3),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.1),
|
||
|
|
blurRadius: 20,
|
||
|
|
offset: const Offset(0, 10),
|
||
|
|
spreadRadius: 5,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
'Welcome Back!',
|
||
|
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||
|
|
color: colorScheme.primary,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
letterSpacing: 0.5,
|
||
|
|
),
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
),
|
||
|
|
const SizedBox(height: UIConstants.spacing12),
|
||
|
|
Text(
|
||
|
|
'Sign in to continue to your account',
|
||
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
|
|
color: colorScheme.onSurface.withOpacity(0.8),
|
||
|
|
fontWeight: FontWeight.w400,
|
||
|
|
),
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildLoginForm(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _formAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return Transform.scale(
|
||
|
|
scale: _formScaleAnimation.value,
|
||
|
|
child: Container(
|
||
|
|
width: UIConstants.getResponsiveValue(
|
||
|
|
context,
|
||
|
|
mobile: double.infinity,
|
||
|
|
tablet: 450,
|
||
|
|
desktop: 500,
|
||
|
|
),
|
||
|
|
padding: UIConstants.cardPaddingLarge,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.95),
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius24),
|
||
|
|
border: Border.all(
|
||
|
|
color: Colors.white.withOpacity(0.3),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.15),
|
||
|
|
blurRadius: 25,
|
||
|
|
offset: const Offset(0, 15),
|
||
|
|
spreadRadius: 8,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
// Email Field
|
||
|
|
_buildEnhancedTextField(
|
||
|
|
label: 'Email Address',
|
||
|
|
hint: 'Enter your email',
|
||
|
|
controller: _emailController,
|
||
|
|
focusNode: _emailFocusNode,
|
||
|
|
keyboardType: TextInputType.emailAddress,
|
||
|
|
prefixIcon: Icons.email_outlined,
|
||
|
|
isFocused: _isEmailFocused,
|
||
|
|
colorScheme: colorScheme,
|
||
|
|
textInputAction: TextInputAction.next,
|
||
|
|
onSubmitted: (_) => _passwordFocusNode.requestFocus(),
|
||
|
|
validator: (value) {
|
||
|
|
if (value == null || value.isEmpty) {
|
||
|
|
return 'Email is required';
|
||
|
|
}
|
||
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||
|
|
.hasMatch(value)) {
|
||
|
|
return 'Please enter a valid email';
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: UIConstants.spacing24),
|
||
|
|
|
||
|
|
// Password Field
|
||
|
|
_buildEnhancedTextField(
|
||
|
|
label: 'Password',
|
||
|
|
hint: 'Enter your password',
|
||
|
|
controller: _passwordController,
|
||
|
|
focusNode: _passwordFocusNode,
|
||
|
|
obscureText: !_isPasswordVisible,
|
||
|
|
prefixIcon: Icons.lock_outlined,
|
||
|
|
isFocused: _isPasswordFocused,
|
||
|
|
colorScheme: colorScheme,
|
||
|
|
suffixIcon: IconButton(
|
||
|
|
icon: Icon(
|
||
|
|
_isPasswordVisible
|
||
|
|
? Icons.visibility
|
||
|
|
: Icons.visibility_off,
|
||
|
|
color: colorScheme.onSurface.withOpacity(0.6),
|
||
|
|
),
|
||
|
|
onPressed: () {
|
||
|
|
setState(() {
|
||
|
|
_isPasswordVisible = !_isPasswordVisible;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
),
|
||
|
|
textInputAction: TextInputAction.done,
|
||
|
|
validator: (value) {
|
||
|
|
if (value == null || value.isEmpty) {
|
||
|
|
return 'Password is required';
|
||
|
|
}
|
||
|
|
if (value.length < 6) {
|
||
|
|
return 'Password must be at least 6 characters';
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: UIConstants.spacing16),
|
||
|
|
|
||
|
|
// Forgot Password - Now below password field
|
||
|
|
_buildForgotPasswordSection(colorScheme),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildEnhancedTextField({
|
||
|
|
required String label,
|
||
|
|
required String hint,
|
||
|
|
required TextEditingController controller,
|
||
|
|
required FocusNode focusNode,
|
||
|
|
required IconData prefixIcon,
|
||
|
|
required bool isFocused,
|
||
|
|
required ColorScheme colorScheme,
|
||
|
|
bool obscureText = false,
|
||
|
|
Widget? suffixIcon,
|
||
|
|
TextInputType? keyboardType,
|
||
|
|
TextInputAction? textInputAction,
|
||
|
|
Function(String)? onSubmitted,
|
||
|
|
String? Function(String?)? validator,
|
||
|
|
}) {
|
||
|
|
return Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||
|
|
border: Border.all(
|
||
|
|
color: isFocused ? colorScheme.primary : Colors.grey.withOpacity(0.3),
|
||
|
|
width: isFocused ? 2 : 1,
|
||
|
|
),
|
||
|
|
boxShadow: isFocused
|
||
|
|
? [
|
||
|
|
BoxShadow(
|
||
|
|
color: colorScheme.primary.withOpacity(0.2),
|
||
|
|
blurRadius: 12,
|
||
|
|
spreadRadius: 3,
|
||
|
|
),
|
||
|
|
]
|
||
|
|
: null,
|
||
|
|
),
|
||
|
|
child: ModernTextField(
|
||
|
|
label: label,
|
||
|
|
hint: hint,
|
||
|
|
controller: controller,
|
||
|
|
focusNode: focusNode,
|
||
|
|
obscureText: obscureText,
|
||
|
|
keyboardType: keyboardType,
|
||
|
|
prefixIcon: Icon(
|
||
|
|
prefixIcon,
|
||
|
|
color: isFocused ? colorScheme.primary : Colors.grey.withOpacity(0.7),
|
||
|
|
),
|
||
|
|
suffixIcon: suffixIcon,
|
||
|
|
textInputAction: textInputAction,
|
||
|
|
onSubmitted: onSubmitted,
|
||
|
|
validator: validator,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildForgotPasswordSection(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _formAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return FadeTransition(
|
||
|
|
opacity: _formScaleAnimation,
|
||
|
|
child: Align(
|
||
|
|
alignment: Alignment.centerLeft,
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: UIConstants.spacing12,
|
||
|
|
vertical: UIConstants.spacing6,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.9),
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||
|
|
border: Border.all(
|
||
|
|
color: colorScheme.primary.withOpacity(0.3),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.08),
|
||
|
|
blurRadius: 6,
|
||
|
|
offset: const Offset(0, 2),
|
||
|
|
spreadRadius: 1,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
// Handle forgot password action
|
||
|
|
},
|
||
|
|
style: TextButton.styleFrom(
|
||
|
|
padding: EdgeInsets.zero,
|
||
|
|
minimumSize: Size.zero,
|
||
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(
|
||
|
|
Icons.help_outline,
|
||
|
|
color: colorScheme.primary,
|
||
|
|
size: UIConstants.iconSizeSmall,
|
||
|
|
),
|
||
|
|
const SizedBox(width: UIConstants.spacing6),
|
||
|
|
Text(
|
||
|
|
'Forgot Password?',
|
||
|
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||
|
|
color: colorScheme.primary,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
fontSize: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildLoginButton(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _formAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return Transform.scale(
|
||
|
|
scale: _formScaleAnimation.value,
|
||
|
|
child: Container(
|
||
|
|
width: UIConstants.getResponsiveValue(
|
||
|
|
context,
|
||
|
|
mobile: double.infinity,
|
||
|
|
tablet: 450,
|
||
|
|
desktop: 500,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: colorScheme.primary.withOpacity(0.3),
|
||
|
|
blurRadius: 20,
|
||
|
|
offset: const Offset(0, 10),
|
||
|
|
spreadRadius: 5,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Consumer<AuthViewModel>(
|
||
|
|
builder: (context, provider, child) {
|
||
|
|
return ModernButton(
|
||
|
|
text: 'Sign In',
|
||
|
|
type: ModernButtonType.primary,
|
||
|
|
size: ModernButtonSize.large,
|
||
|
|
isLoading: provider.isLoading,
|
||
|
|
isDisabled: !_isFormValid,
|
||
|
|
onPressed: _isFormValid
|
||
|
|
? () {
|
||
|
|
final data = {
|
||
|
|
"email": _emailController.text,
|
||
|
|
"password": _passwordController.text,
|
||
|
|
};
|
||
|
|
provider.login(context, data);
|
||
|
|
}
|
||
|
|
: null,
|
||
|
|
icon: Icon(Icons.login),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSignUpSection(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _formAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return FadeTransition(
|
||
|
|
opacity: _formScaleAnimation,
|
||
|
|
child: Container(
|
||
|
|
padding: UIConstants.cardPaddingMedium,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.9),
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||
|
|
border: Border.all(
|
||
|
|
color: Colors.white.withOpacity(0.3),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.1),
|
||
|
|
blurRadius: 15,
|
||
|
|
offset: const Offset(0, 8),
|
||
|
|
spreadRadius: 3,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
"Don't have an account? ",
|
||
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
|
|
color: Colors.grey.shade700,
|
||
|
|
fontWeight: FontWeight.w500,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.pushNamed(context, RouteNames.getOtpView);
|
||
|
|
},
|
||
|
|
child: Text(
|
||
|
|
'Sign Up',
|
||
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||
|
|
color: colorScheme.primary,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
fontSize: 16,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSocialLoginSection(ColorScheme colorScheme) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _formAnimationController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return FadeTransition(
|
||
|
|
opacity: _formScaleAnimation,
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
Expanded(
|
||
|
|
child: Container(
|
||
|
|
height: 1,
|
||
|
|
color: Colors.white.withOpacity(0.4),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: UIConstants.spacing16),
|
||
|
|
child: Text(
|
||
|
|
'Or continue with',
|
||
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
|
|
color: Colors.white.withOpacity(0.9),
|
||
|
|
fontWeight: FontWeight.w500,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Expanded(
|
||
|
|
child: Container(
|
||
|
|
height: 1,
|
||
|
|
color: Colors.white.withOpacity(0.4),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: UIConstants.spacing24),
|
||
|
|
Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
_buildSocialButton(
|
||
|
|
icon: Icons.g_mobiledata,
|
||
|
|
label: 'Google',
|
||
|
|
colorScheme: colorScheme,
|
||
|
|
onPressed: () {
|
||
|
|
// Handle Google sign in
|
||
|
|
},
|
||
|
|
),
|
||
|
|
const SizedBox(width: UIConstants.spacing16),
|
||
|
|
_buildSocialButton(
|
||
|
|
icon: Icons.apple,
|
||
|
|
label: 'Apple',
|
||
|
|
colorScheme: colorScheme,
|
||
|
|
onPressed: () {
|
||
|
|
// Handle Apple sign in
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSocialButton({
|
||
|
|
required IconData icon,
|
||
|
|
required String label,
|
||
|
|
required ColorScheme colorScheme,
|
||
|
|
required VoidCallback onPressed,
|
||
|
|
}) {
|
||
|
|
return Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.95),
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||
|
|
border: Border.all(
|
||
|
|
color: Colors.white.withOpacity(0.3),
|
||
|
|
width: 1,
|
||
|
|
),
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.1),
|
||
|
|
blurRadius: 10,
|
||
|
|
offset: const Offset(0, 5),
|
||
|
|
spreadRadius: 2,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Material(
|
||
|
|
color: Colors.transparent,
|
||
|
|
child: InkWell(
|
||
|
|
onTap: onPressed,
|
||
|
|
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: UIConstants.spacing20,
|
||
|
|
vertical: UIConstants.spacing12,
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(
|
||
|
|
icon,
|
||
|
|
size: UIConstants.iconSizeMedium,
|
||
|
|
color: colorScheme.primary,
|
||
|
|
),
|
||
|
|
const SizedBox(width: UIConstants.spacing8),
|
||
|
|
Text(
|
||
|
|
label,
|
||
|
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||
|
|
color: colorScheme.primary,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ParticlePainter extends CustomPainter {
|
||
|
|
final double rotation;
|
||
|
|
final Color color;
|
||
|
|
|
||
|
|
ParticlePainter({required this.rotation, required this.color});
|
||
|
|
|
||
|
|
@override
|
||
|
|
void paint(Canvas canvas, Size size) {
|
||
|
|
final paint = Paint()
|
||
|
|
..color = color
|
||
|
|
..style = PaintingStyle.fill;
|
||
|
|
|
||
|
|
final center = Offset(size.width / 2, size.height / 2);
|
||
|
|
final radius = math.min(size.width, size.height) / 2.5;
|
||
|
|
|
||
|
|
// Draw rotating particles
|
||
|
|
for (int i = 0; i < 12; i++) {
|
||
|
|
final angle = (i * math.pi / 6) + rotation;
|
||
|
|
final x = center.dx + radius * math.cos(angle);
|
||
|
|
final y = center.dy + radius * math.sin(angle);
|
||
|
|
|
||
|
|
canvas.drawCircle(
|
||
|
|
Offset(x, y),
|
||
|
|
4,
|
||
|
|
paint,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool shouldRepaint(ParticlePainter oldDelegate) => true;
|
||
|
|
}
|