import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/api.dart'; import '../../core/providers.dart'; import '../../core/theme.dart'; import '../../widgets/ui.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _user = TextEditingController(); final _pass = TextEditingController(); final _scroll = ScrollController(); bool _busy = false; bool _keyboard = false; String? _error; @override void dispose() { _user.dispose(); _pass.dispose(); _scroll.dispose(); super.dispose(); } void _followKeyboard(BuildContext context) { final open = MediaQuery.viewInsetsOf(context).bottom > 0; if (open && !_keyboard) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!_scroll.hasClients) return; _scroll.animateTo( _scroll.position.maxScrollExtent, duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); }); } _keyboard = open; } Future _submit() async { if (_busy) return; if (_user.text.trim().isEmpty || _pass.text.isEmpty) { setState(() => _error = 'Enter your username and password'); return; } setState(() { _busy = true; _error = null; }); try { await ref.read(sessionProvider.notifier).login(_user.text, _pass.text); } on ApiException catch (e) { if (mounted) setState(() => _error = e.message); } catch (_) { if (mounted) { setState(() => _error = 'Unable to sign in. Please try again.'); } } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final screen = MediaQuery.sizeOf(context); final landscape = screen.width > screen.height && screen.width >= 600; if (screen.width >= 900 || landscape) { return Scaffold( backgroundColor: C.navy, body: Row( children: [ Expanded(child: _BrandPanel(large: screen.height >= 560)), Container( width: screen.width >= 1280 ? 560 : (screen.width >= 900 ? 480 : 420), color: C.bg, child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(S.s36), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 380), child: _form(autofocus: true), ), ), ), ), ], ), ); } final hero = (screen.height * 0.36).clamp(230.0, 340.0); _followKeyboard(context); return Scaffold( backgroundColor: C.bg, body: CustomScrollView( controller: _scroll, physics: const ClampingScrollPhysics(), slivers: [ SliverToBoxAdapter( child: ColoredBox( color: C.navy, child: SizedBox( height: hero, child: const _BrandPanel(large: false), ), ), ), SliverFillRemaining( hasScrollBody: false, child: ColoredBox( color: C.navy, child: Container( decoration: const BoxDecoration( color: C.bg, borderRadius: BorderRadius.vertical( top: Radius.circular(R.hero), ), ), padding: const EdgeInsets.fromLTRB( S.s24, S.s28, S.s24, S.s24, ), alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: _form(autofocus: false), ), ), ), ), ], ), ); } Widget _form({required bool autofocus}) => AutofillGroup( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Welcome back', style: ts(24, w: w7, ls: -0.3)), const SizedBox(height: S.s4), Text('Sign in to continue', style: ts(14, c: C.muted)), const SizedBox(height: S.s24), SrsField( controller: _user, label: 'Username', hint: 'Enter username', error: _error != null, enabled: !_busy, autofocus: autofocus, autofill: const [AutofillHints.username], onChanged: (_) => setState(() => _error = null), ), const SizedBox(height: S.s16), SrsField( controller: _pass, label: 'Password', hint: 'Enter password', password: true, error: _error != null, enabled: !_busy, autofill: const [AutofillHints.password], onChanged: (_) => setState(() => _error = null), onSubmitted: (_) => _submit(), ), if (_error != null) ...[ const SizedBox(height: S.s12), ErrorLine(_error!), ], const SizedBox(height: S.s22), BigButton('Sign in', loading: _busy, onPressed: _submit), const SizedBox(height: S.s18), Center( child: Text( 'Forgot your password? Contact your administrator.', textAlign: TextAlign.center, style: ts(13, c: C.muted), ), ), ], ), ); } class _BrandPanel extends StatelessWidget { const _BrandPanel({required this.large}); final bool large; @override Widget build(BuildContext context) { final coin = large ? 112.0 : 84.0; return DecoratedBox( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [C.navy, C.navyDeep], ), ), child: Stack( children: [ Positioned( right: large ? -120 : -50, top: large ? -80 : -30, child: _ring(large ? 420 : 190, C.onNavy8), ), Positioned( left: large ? -90 : -40, bottom: large ? -60 : 10, child: _ring(large ? 300 : 120, C.onNavy5), ), SafeArea( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: coin, height: coin, decoration: const BoxDecoration( color: C.gold, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: C.goldGlow, blurRadius: 30, offset: Offset(0, 10), ), ], ), child: Icon( Icons.confirmation_number_rounded, color: C.navy, size: coin * 0.48, ), ), SizedBox(height: large ? S.s24 : S.s18), Text( 'SRS', style: ts(large ? 44 : 34, w: w7, c: Colors.white, ls: 2), ), const SizedBox(height: S.s6), Text( 'Your sequences, at a glance', style: ts(large ? 16 : 14, c: C.softOnNavy), ), ], ), ), ), ], ), ); } Widget _ring(double size, Color color) => Container( width: size, height: size, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ); }