hwhub/lib/screens/login_screen.dart

349 lines
14 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import '../main.dart';
import '../theme/app_colors.dart';
/// Branded login screen matching the car64 style.
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
late final AnimationController _fadeController;
late final Animation<double> _fadeAnim;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_fadeAnim = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeOut,
);
_fadeController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// ── Auth actions ─────────────────────────────────────────────────────
Future<void> _signIn() async {
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) return;
setState(() => _isLoading = true);
try {
await supabase.auth.signInWithPassword(
email: email,
password: password,
);
} on AuthException catch (e) {
if (!mounted) return;
showGlobalSnackBar(e.message, isError: true);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _forgotPassword() async {
final email = _emailController.text.trim();
if (email.isEmpty) {
showGlobalSnackBar('Enter your email first.');
return;
}
try {
await supabase.auth.resetPasswordForEmail(
email,
redirectTo: 'hwcollector://login/recovery',
);
showGlobalSuccess('Password reset email sent! Check your inbox.');
} on AuthException catch (e) {
showGlobalSnackBar(e.message, isError: true);
}
}
// ── Build ────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).viewInsets.bottom;
final topPadding = MediaQuery.of(context).padding.top;
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
// ── Full-screen background image ──
Image.asset(
'assets/img/login_bg.jpg',
fit: BoxFit.cover,
),
// ── Dark overlay for readability ──
Container(
color: Colors.black.withValues(alpha: 0.45),
),
// ── Scrollable content ──
SingleChildScrollView(
padding: EdgeInsets.only(bottom: bottom),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: topPadding + 40),
// ── Brand title ──
FadeTransition(
opacity: _fadeAnim,
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Image.asset(
'assets/img/icon_bg_removed.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.directions_car_filled,
size: 44,
color: Colors.white,
),
),
),
),
const SizedBox(height: 16),
const Text(
'CAR64',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 3,
),
),
const SizedBox(height: 6),
Text(
'Track your collection',
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: 0.8),
letterSpacing: 0.5,
),
),
],
),
),
const SizedBox(height: 36),
// ── Frosted glass form card ──
FadeTransition(
opacity: _fadeAnim,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 24),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: Column(
children: [
// Email
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
style: const TextStyle(color: Colors.white),
decoration: _glassInputDecoration(
label: 'Email',
icon: Icons.email_outlined,
),
),
const SizedBox(height: 16),
// Password
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _signIn(),
style: const TextStyle(color: Colors.white),
decoration: _glassInputDecoration(
label: 'Password',
icon: Icons.lock_outline,
).copyWith(
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.white70,
),
onPressed: () => setState(
() => _obscurePassword =
!_obscurePassword),
),
),
),
const SizedBox(height: 8),
// Forgot password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _forgotPassword,
child: Text(
'Forgot Password?',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 13,
),
),
),
),
const SizedBox(height: 12),
// Sign-in button (gradient)
SizedBox(
width: double.infinity,
height: 52,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: AppColors.brandGradient,
borderRadius: BorderRadius.circular(50),
boxShadow: [
BoxShadow(
color: AppColors.orange
.withValues(alpha: 0.4),
blurRadius: 16,
offset: const Offset(0, 5),
),
],
),
child: ElevatedButton(
onPressed: _isLoading ? null : _signIn,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(50),
),
),
child: _isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Start Your Engines!',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
),
),
),
],
),
),
),
),
),
),
const SizedBox(height: 32),
// ── Footer ──
Text(
'© 2026 car64',
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: 0.5),
),
),
const SizedBox(height: 24),
],
),
),
),
],
),
);
}
/// Input decoration styled for the frosted-glass card.
InputDecoration _glassInputDecoration({
required String label,
required IconData icon,
}) {
return InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
prefixIcon: Icon(icon, color: Colors.white70),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.08),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.15)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: AppColors.orange, width: 2),
),
);
}
}
// _GradientHeader is no longer needed — removed.