Files
repertory/web/repertory/lib/screens/auth_screen.dart
Scott E. Graves b8dd642dc5
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
[ui] UI theme should match repertory blue #61
2025-08-17 09:25:34 -05:00

345 lines
14 KiB
Dart

// auth_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
class AuthScreen extends StatefulWidget {
final String title;
const AuthScreen({super.key, required this.title});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _userController = TextEditingController();
bool _enabled = true;
bool _obscure = true;
@override
void dispose() {
_passwordController.dispose();
_userController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
InputDecoration decoration(String label, IconData icon) => InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
filled: true,
fillColor: scheme.primary.withValues(alpha: 0.15),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide(color: scheme.primary, width: 2),
),
contentPadding: const EdgeInsets.all(constants.paddingSmall),
);
Future<void> doLogin(Auth auth) async {
if (!_enabled) {
return;
}
if (!_formKey.currentState!.validate()) {
return;
}
setState(() => _enabled = false);
final authenticated = await auth.authenticate(
_userController.text.trim(),
_passwordController.text,
);
setState(() => _enabled = true);
if (authenticated) {
return;
}
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid username or password'),
behavior: SnackBarBehavior.floating,
),
);
}
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
stops: [0.0, 1.0],
),
),
),
const AuroraSweep(),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(color: Colors.black.withValues(alpha: 0.10)),
),
),
Align(
alignment: const Alignment(0, 0.06),
child: IgnorePointer(
child: Container(
width: 720,
height: 720,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
scheme.primary.withValues(alpha: 0.20),
Colors.transparent,
],
stops: const [0.0, 1.0],
),
),
),
),
),
Consumer<Auth>(
builder: (context, auth, _) {
if (auth.authenticated) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/', (r) => false);
});
return const SizedBox.shrink();
}
return Center(
child: AnimatedScale(
scale: 1.0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
child: AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 250),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 420,
minWidth: 300,
),
child: Card(
elevation: constants.padding,
color: scheme.primary.withValues(alpha: 0.10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
),
child: Padding(
padding: const EdgeInsets.all(
constants.padding * 2.0,
),
child: Form(
key: _formKey,
autovalidateMode:
AutovalidateMode.onUserInteraction,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.center,
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: scheme.primary.withValues(
alpha: 0.11,
),
borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
),
boxShadow: [
BoxShadow(
color: scheme.primary.withValues(
alpha: 0.08,
),
blurRadius: 16,
spreadRadius: 1,
),
],
),
padding: const EdgeInsets.all(
constants.borderRadiusSmall,
),
child: Image.asset(
'assets/images/repertory.png',
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
color: scheme.primary,
size: 40,
);
},
),
),
),
const SizedBox(
height: constants.paddingSmall,
),
Text(
constants.appLogonTitle,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(
height: constants.paddingSmall,
),
Text(
"Secure access to your mounts",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: scheme.onSurface.withValues(
alpha: 0.72,
),
),
),
const SizedBox(
height: constants.padding * 2.0,
),
TextFormField(
autofocus: true,
controller: _userController,
textInputAction: TextInputAction.next,
decoration: decoration(
'Username',
Icons.person,
),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Enter your username';
}
return null;
},
onFieldSubmitted: (_) {
FocusScope.of(context).nextFocus();
},
),
const SizedBox(height: constants.padding),
TextFormField(
controller: _passwordController,
obscureText: _obscure,
textInputAction: TextInputAction.go,
decoration:
decoration(
'Password',
Icons.lock,
).copyWith(
suffixIcon: IconButton(
tooltip: _obscure
? 'Show password'
: 'Hide password',
icon: Icon(
_obscure
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscure = !_obscure;
});
},
),
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Enter your password';
}
return null;
},
onFieldSubmitted: (_) {
doLogin(auth);
},
),
const SizedBox(
height: constants.padding * 2.0,
),
SizedBox(
height: 46,
child: ElevatedButton(
onPressed: _enabled
? () {
doLogin(auth);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary
.withValues(alpha: 0.45),
disabledBackgroundColor: scheme.primary
.withValues(alpha: 0.15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
),
),
),
child: _enabled
? const Text('Login')
: const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2.4,
),
),
),
),
],
),
),
),
),
),
),
),
);
},
),
],
),
),
);
}
}