From e9a5b7e9af7e9e67efb48f2a8aa8916df20b1a85 Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Fri, 15 Aug 2025 10:59:12 -0500 Subject: [PATCH] [ui] UI theme should match repertory blue #61 --- web/repertory/.cspell/words.txt | 1 + web/repertory/assets/images/repertory.png | Bin 0 -> 368 bytes web/repertory/lib/screens/auth_screen.dart | 335 +++++++++++++-------- web/repertory/pubspec.yaml | 5 +- 4 files changed, 208 insertions(+), 133 deletions(-) create mode 100644 web/repertory/assets/images/repertory.png diff --git a/web/repertory/.cspell/words.txt b/web/repertory/.cspell/words.txt index deb8473d..7330ecc9 100644 --- a/web/repertory/.cspell/words.txt +++ b/web/repertory/.cspell/words.txt @@ -1,4 +1,5 @@ autofocus +autovalidatemode canvaskit cupertino cupertinoicons diff --git a/web/repertory/assets/images/repertory.png b/web/repertory/assets/images/repertory.png new file mode 100644 index 0000000000000000000000000000000000000000..ac69855c82d2c65bd3865b2061146e27324ab078 GIT binary patch literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9F5M?jcysy3fA0|TRy zr;B4q#jUs3Hs(4RO0-^_ePWTv#7Rv}T&dk}9K;+r+7cMF*c6yk7@sZZaA^z*P~_rr z@%5c7s1%noH=k+${~tH&q-tu;PTJ!hTcWl9@cdb>>Tl=AGm(G^FF^FIO%=Yv_1N~|1Pc< zUHkp0;1fQqrr;I467IRlW7C?9v)h+3oSS`8_2kjG5*9|FBdR{A+RVuh_dM2HJn2 { - bool _enabled = true; + final _formKey = GlobalKey(); final _passwordController = TextEditingController(); final _userController = TextEditingController(); + bool _enabled = true; + bool _obscure = true; + + @override + void dispose() { + _passwordController.dispose(); + _userController.dispose(); + super.dispose(); + } + @override Widget build(context) { final scheme = Theme.of(context).colorScheme; @@ -36,156 +46,221 @@ class _AuthScreenState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), ); - VoidCallback? createLoginHandler(Auth auth) { - if (!_enabled) return null; - return () async { - setState(() => _enabled = false); - await auth.authenticate(_userController.text, _passwordController.text); - setState(() => _enabled = true); - }; + Future doLogin(Auth auth) async { + if (!_enabled) { + return; + } + + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() => _enabled = false); + await auth.authenticate( + _userController.text.trim(), + _passwordController.text, + ); + setState(() => _enabled = true); + + // if (!context.mounted) return; + // + // // Only show the error if login failed + // if (!ok && !auth.authenticated) { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Invalid username or password'), + // behavior: SnackBarBehavior.floating, + // ), + // ); + // } } return Scaffold( appBar: AppBar(title: Text(widget.title), scrolledUnderElevation: 0), - body: Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF1050A0), Color(0xFF202124)], + 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, + Theme.of(context).colorScheme.surface, + ], + stops: const [0.0, 1.0], + ), ), - ), - child: Consumer( - builder: (context, auth, _) { - if (auth.authenticated) { - Future.delayed(Duration(milliseconds: 1), () { - if (constants.navigatorKey.currentContext == null) { - return; - } + 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('/', (Route route) => false); - }); + Navigator.of( + constants.navigatorKey.currentContext!, + ).pushNamedAndRemoveUntil('/', (r) => false); + }); - return const SizedBox.shrink(); - } + return const SizedBox.shrink(); + } - return Center( - child: AnimatedScale( - scale: 1.0, - duration: const Duration(milliseconds: 250), - curve: Curves.easeOutCubic, - child: AnimatedOpacity( - opacity: 1.0, + return Center( + child: AnimatedScale( + scale: 1.0, duration: const Duration(milliseconds: 250), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 420, - minWidth: 300, - ), - child: Card( - elevation: 12, - color: scheme.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), + curve: Curves.easeOutCubic, + child: AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 250), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 420, + minWidth: 300, ), - child: Padding( - padding: const EdgeInsets.all(constants.padding), - 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: 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, + child: Card( + elevation: 12, + color: scheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(constants.padding), + child: Form( + 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, ), - ), - ), - const SizedBox(height: 20), - TextField( - autofocus: true, - controller: _userController, - textInputAction: TextInputAction.next, - decoration: decoration('Username', Icons.person), - ), - const SizedBox(height: constants.padding), - TextField( - controller: _passwordController, - obscureText: true, - textInputAction: TextInputAction.go, - onSubmitted: (_) { - final handler = createLoginHandler(auth); - handler?.call(); - }, - decoration: decoration('Password', Icons.lock), - ), - const SizedBox(height: constants.padding), - SizedBox( - height: 44, - child: ElevatedButton( - onPressed: createLoginHandler(auth), - 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, + 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, + ), + 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, + ), + ), + ), + 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, + ), + ), + ), + ), + ], ), - ], + ), ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); } - - @override - void setState(VoidCallback fn) { - if (!mounted) return; - super.setState(fn); - } } diff --git a/web/repertory/pubspec.yaml b/web/repertory/pubspec.yaml index f7d56c69..756de0d0 100644 --- a/web/repertory/pubspec.yaml +++ b/web/repertory/pubspec.yaml @@ -64,9 +64,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/images/repertory.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images