From 14998bf9527f68f8ef172332b2468ffe7aca8bb4 Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Fri, 15 Aug 2025 12:29:42 -0500 Subject: [PATCH] [ui] UI theme should match repertory blue #61 --- web/repertory/.cspell/words.txt | 3 +- web/repertory/lib/screens/auth_screen.dart | 386 ++++++++++++++------- 2 files changed, 255 insertions(+), 134 deletions(-) diff --git a/web/repertory/.cspell/words.txt b/web/repertory/.cspell/words.txt index 7330ecc9..b93e72f2 100644 --- a/web/repertory/.cspell/words.txt +++ b/web/repertory/.cspell/words.txt @@ -6,4 +6,5 @@ cupertinoicons fromargb onetwothree renterd -rocksdb \ No newline at end of file +rocksdb +vsync \ No newline at end of file diff --git a/web/repertory/lib/screens/auth_screen.dart b/web/repertory/lib/screens/auth_screen.dart index 12945ef5..eac69998 100644 --- a/web/repertory/lib/screens/auth_screen.dart +++ b/web/repertory/lib/screens/auth_screen.dart @@ -1,3 +1,4 @@ +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:repertory/constants.dart' as constants; @@ -62,10 +63,9 @@ class _AuthScreenState extends State { ); setState(() => _enabled = true); - // if (!context.mounted) return; + // if (!mounted) return; // - // // Only show the error if login failed - // if (!ok && !auth.authenticated) { + // if (!(ok == true || auth.authenticated)) { // ScaffoldMessenger.of(context).showSnackBar( // const SnackBar( // content: Text('Invalid username or password'), @@ -75,36 +75,38 @@ class _AuthScreenState extends State { // } } + void navigateHomePostFrame() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final navCtx = constants.navigatorKey.currentContext; + if (navCtx == null) { + return; + } + Navigator.of(navCtx).pushNamedAndRemoveUntil('/', (r) => false); + }); + } + return Scaffold( - appBar: AppBar(title: Text(widget.title), scrolledUnderElevation: 0), - body: SafeArea( - child: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), - Theme.of(context).colorScheme.surface.withValues(alpha: 0.9), - ], - stops: const [0.0, 1.0], - ), + appBar: AppBar( + title: Text(widget.title), + scrolledUnderElevation: 0, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.55), + elevation: 0, + flexibleSpace: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), + child: const SizedBox.expand(), ), + ), + ), + body: SafeArea( + child: CoolScaffoldBg( + enableAurora: true, child: Consumer( builder: (context, auth, _) { if (auth.authenticated) { - Future.delayed(const Duration(milliseconds: 1), () { - if (constants.navigatorKey.currentContext == null) { - return; - } - - Navigator.of( - constants.navigatorKey.currentContext!, - ).pushNamedAndRemoveUntil('/', (r) => false); - }); - + navigateHomePostFrame(); return const SizedBox.shrink(); } @@ -133,122 +135,134 @@ class _AuthScreenState extends State { key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Align( - alignment: Alignment.center, - child: CircleAvatar( - radius: 28, - backgroundColor: scheme.primary.withValues( - alpha: 0.18, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(28), - child: Image.asset( - 'assets/images/repertory.png', - width: 28, - height: 28, - fit: BoxFit.contain, - errorBuilder: (_, _, _) => Icon( - Icons.folder, - color: scheme.primary, - size: 28, + child: AutofillGroup( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.center, + child: CircleAvatar( + radius: 28, + backgroundColor: scheme.primary + .withValues(alpha: 0.18), + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Image.asset( + 'assets/images/repertory.png', + width: 28, + height: 28, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => Icon( + Icons.folder, + color: scheme.primary, + size: 28, + ), ), ), ), ), - ), - const SizedBox(height: 14), - Text( - constants.appLogonTitle, - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - Text( - "Secure access to your mounts", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: scheme.onSurface.withValues( - alpha: 0.7, - ), - ), - ), - const SizedBox(height: 20), - - TextFormField( - autofocus: true, - controller: _userController, - textInputAction: TextInputAction.next, - decoration: decoration( - 'Username', - Icons.person, + const SizedBox(height: 14), + Text( + constants.appLogonTitle, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.w600), ), - validator: (v) => - (v == null || v.trim().isEmpty) - ? 'Enter your username' - : 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, + const SizedBox(height: 6), + Text( + "Secure access to your mounts", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: scheme.onSurface.withValues( + alpha: 0.7, ), ), - ), - validator: (v) => (v == null || v.isEmpty) - ? 'Enter your password' - : null, - onFieldSubmitted: (_) => doLogin(auth), - ), - const SizedBox(height: constants.padding), - - SizedBox( - height: 44, - child: ElevatedButton( - onPressed: _enabled - ? () => doLogin(auth) + ), + const SizedBox(height: 20), + TextFormField( + autofocus: true, + controller: _userController, + textInputAction: TextInputAction.next, + autofillHints: const [ + AutofillHints.username, + ], + decoration: decoration( + 'Username', + Icons.person, + ), + validator: (v) => + (v == null || v.trim().isEmpty) + ? 'Enter your username' : null, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _enabled - ? const Text('Login') - : const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2.4, + onFieldSubmitted: (_) => + FocusScope.of(context).nextFocus(), + ), + const SizedBox(height: constants.padding), + TextFormField( + controller: _passwordController, + obscureText: _obscure, + textInputAction: TextInputAction.go, + autofillHints: const [ + AutofillHints.password, + ], + 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) => (v == null || v.isEmpty) + ? 'Enter your password' + : null, + onFieldSubmitted: (_) => doLogin(auth), ), - ), - ], + const SizedBox(height: constants.padding), + + SizedBox( + height: 44, + child: ElevatedButton( + onPressed: _enabled + ? () => doLogin(auth) + : null, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 12, + ), + ), + ), + child: _enabled + ? const Text('Login') + : const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2.4, + ), + ), + ), + ), + ], + ), ), ), ), @@ -264,3 +278,109 @@ class _AuthScreenState extends State { ); } } + +class CoolScaffoldBg extends StatelessWidget { + final Widget child; + final bool enableAurora; + const CoolScaffoldBg({ + super.key, + required this.child, + this.enableAurora = true, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return Stack( + children: [ + Builder( + builder: (context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.surface, + ], + ), + ), + ); + }, + ), + if (enableAurora) const AuroraSweep(), + Align( + alignment: const Alignment(0, 0.1), + child: IgnorePointer( + child: Container( + width: 740, + height: 740, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + scheme.primary.withValues(alpha: 0.22), + Colors.transparent, + ], + stops: const [0.0, 1.0], + ), + ), + ), + ), + ), + child, + ], + ); + } +} + +class AuroraSweep extends StatefulWidget { + const AuroraSweep({super.key}); + + @override + State createState() => _AuroraSweepState(); +} + +class _AuroraSweepState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController( + vsync: this, + duration: const Duration(seconds: 18), + )..repeat(); + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return AnimatedBuilder( + animation: _c, + builder: (_, _) { + final t = _c.value; + return IgnorePointer( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(-1 + 2 * t, -0.6), + end: Alignment(0.8 - 2 * t, 0.9), + colors: [ + scheme.primary.withValues(alpha: 0.06), + Colors.transparent, + scheme.primary.withValues(alpha: 0.04), + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ); + }, + ); + } +}