[ui] UI theme should match repertory blue #61
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good

This commit is contained in:
2025-08-15 12:29:42 -05:00
parent 60222b3948
commit 14998bf952
2 changed files with 255 additions and 134 deletions

View File

@@ -6,4 +6,5 @@ cupertinoicons
fromargb fromargb
onetwothree onetwothree
renterd renterd
rocksdb rocksdb
vsync

View File

@@ -1,3 +1,4 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
@@ -62,10 +63,9 @@ class _AuthScreenState extends State<AuthScreen> {
); );
setState(() => _enabled = true); setState(() => _enabled = true);
// if (!context.mounted) return; // if (!mounted) return;
// //
// // Only show the error if login failed // if (!(ok == true || auth.authenticated)) {
// if (!ok && !auth.authenticated) {
// ScaffoldMessenger.of(context).showSnackBar( // ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar( // const SnackBar(
// content: Text('Invalid username or password'), // content: Text('Invalid username or password'),
@@ -75,36 +75,38 @@ class _AuthScreenState extends State<AuthScreen> {
// } // }
} }
void navigateHomePostFrame() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final navCtx = constants.navigatorKey.currentContext;
if (navCtx == null) {
return;
}
Navigator.of(navCtx).pushNamedAndRemoveUntil('/', (r) => false);
});
}
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(widget.title), scrolledUnderElevation: 0), appBar: AppBar(
body: SafeArea( title: Text(widget.title),
child: Container( scrolledUnderElevation: 0,
width: double.infinity, backgroundColor: Theme.of(
height: double.infinity, context,
decoration: BoxDecoration( ).colorScheme.surface.withValues(alpha: 0.55),
gradient: LinearGradient( elevation: 0,
begin: Alignment.topLeft, flexibleSpace: ClipRRect(
end: Alignment.bottomRight, child: BackdropFilter(
colors: [ filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14),
Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), child: const SizedBox.expand(),
Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
],
stops: const [0.0, 1.0],
),
), ),
),
),
body: SafeArea(
child: CoolScaffoldBg(
enableAurora: true,
child: Consumer<Auth>( child: Consumer<Auth>(
builder: (context, auth, _) { builder: (context, auth, _) {
if (auth.authenticated) { if (auth.authenticated) {
Future.delayed(const Duration(milliseconds: 1), () { navigateHomePostFrame();
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/', (r) => false);
});
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -133,122 +135,134 @@ class _AuthScreenState extends State<AuthScreen> {
key: _formKey, key: _formKey,
autovalidateMode: autovalidateMode:
AutovalidateMode.onUserInteraction, AutovalidateMode.onUserInteraction,
child: Column( child: AutofillGroup(
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
Align( children: [
alignment: Alignment.center, Align(
child: CircleAvatar( alignment: Alignment.center,
radius: 28, child: CircleAvatar(
backgroundColor: scheme.primary.withValues( radius: 28,
alpha: 0.18, backgroundColor: scheme.primary
), .withValues(alpha: 0.18),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
child: Image.asset( child: Image.asset(
'assets/images/repertory.png', 'assets/images/repertory.png',
width: 28, width: 28,
height: 28, height: 28,
fit: BoxFit.contain, fit: BoxFit.contain,
errorBuilder: (_, _, _) => Icon( errorBuilder: (_, _, _) => Icon(
Icons.folder, Icons.folder,
color: scheme.primary, color: scheme.primary,
size: 28, size: 28,
),
), ),
), ),
), ),
), ),
), const SizedBox(height: 14),
const SizedBox(height: 14), Text(
Text( constants.appLogonTitle,
constants.appLogonTitle, textAlign: TextAlign.center,
textAlign: TextAlign.center, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .headlineSmall
.headlineSmall ?.copyWith(fontWeight: FontWeight.w600),
?.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) => const SizedBox(height: 6),
(v == null || v.trim().isEmpty) Text(
? 'Enter your username' "Secure access to your mounts",
: null, textAlign: TextAlign.center,
onFieldSubmitted: (_) => style: Theme.of(context)
FocusScope.of(context).nextFocus(), .textTheme
), .bodyMedium
const SizedBox(height: constants.padding), ?.copyWith(
color: scheme.onSurface.withValues(
TextFormField( alpha: 0.7,
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) const SizedBox(height: 20),
? 'Enter your password' TextFormField(
: null, autofocus: true,
onFieldSubmitted: (_) => doLogin(auth), controller: _userController,
), textInputAction: TextInputAction.next,
const SizedBox(height: constants.padding), autofillHints: const [
AutofillHints.username,
SizedBox( ],
height: 44, decoration: decoration(
child: ElevatedButton( 'Username',
onPressed: _enabled Icons.person,
? () => doLogin(auth) ),
validator: (v) =>
(v == null || v.trim().isEmpty)
? 'Enter your username'
: null, : null,
style: ElevatedButton.styleFrom( onFieldSubmitted: (_) =>
shape: RoundedRectangleBorder( FocusScope.of(context).nextFocus(),
borderRadius: BorderRadius.circular(12), ),
), const SizedBox(height: constants.padding),
), TextFormField(
child: _enabled controller: _passwordController,
? const Text('Login') obscureText: _obscure,
: const SizedBox( textInputAction: TextInputAction.go,
height: 20, autofillHints: const [
width: 20, AutofillHints.password,
child: CircularProgressIndicator( ],
strokeWidth: 2.4, 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<AuthScreen> {
); );
} }
} }
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<AuroraSweep> createState() => _AuroraSweepState();
}
class _AuroraSweepState extends State<AuroraSweep>
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],
),
),
),
);
},
);
}
}