srs/lib/features/auth/login_screen.dart
Aswin Biju 78c77f708c
All checks were successful
deploy / deploy (push) Successful in 4m2s
fix: updated login screen glitches
2026-11-05 01:39:06 +05:30

273 lines
7.9 KiB
Dart

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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
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<void> _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),
);
}