From 58451858fd603fc9397b640e7dd917ab085444ba Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Thu, 4 Sep 2025 09:09:10 -0500 Subject: [PATCH] [ui] UI theme should match repertory blue #61 --- web/repertory/lib/constants.dart | 1 + web/repertory/lib/helpers.dart | 10 +- web/repertory/lib/screens/home_screen.dart | 5 +- web/repertory/lib/widgets/mount_widget.dart | 539 +++++++++++++------- web/repertory/web/index.html | 5 + 5 files changed, 367 insertions(+), 193 deletions(-) diff --git a/web/repertory/lib/constants.dart b/web/repertory/lib/constants.dart index 60b09904..48f7bd03 100644 --- a/web/repertory/lib/constants.dart +++ b/web/repertory/lib/constants.dart @@ -22,6 +22,7 @@ const protocolTypeList = ['http', 'https']; const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia']; const ringBufferSizeList = ['128', '256', '512', '1024', '2048']; const primaryAlpha = 0.15; +const secondaryAlpha = 0.50; const surfaceContainerLowDark = Color(0xFF292A2D); const surfaceDark = Color(0xFF202124); diff --git a/web/repertory/lib/helpers.dart b/web/repertory/lib/helpers.dart index fff3855e..6eedca38 100644 --- a/web/repertory/lib/helpers.dart +++ b/web/repertory/lib/helpers.dart @@ -16,7 +16,9 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog( return Theme( data: theme.copyWith( dialogTheme: DialogThemeData( - backgroundColor: scheme.primary.withValues(alpha: constants.primaryAlpha), + backgroundColor: scheme.primary.withValues( + alpha: constants.primaryAlpha, + ), surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(constants.borderRadius), @@ -364,11 +366,15 @@ Map getChanged( } Future editMountLocation( - context, + BuildContext context, List available, { bool allowEmpty = false, String? location, }) async { + if (!context.mounted) { + return location; + } + String? currentLocation = location; final controller = TextEditingController(text: currentLocation); return await doShowDialog( diff --git a/web/repertory/lib/screens/home_screen.dart b/web/repertory/lib/screens/home_screen.dart index 905aaa56..52cfc784 100644 --- a/web/repertory/lib/screens/home_screen.dart +++ b/web/repertory/lib/screens/home_screen.dart @@ -179,17 +179,16 @@ class _HomeScreeState extends State { child: Hero( tag: 'add_mount_fab', child: Material( - color: Colors.transparent, + color: scheme.primary.withValues(alpha: constants.secondaryAlpha), elevation: 12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(constants.borderRadius), ), child: Ink( decoration: BoxDecoration( - color: scheme.primary.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(constants.borderRadius), border: Border.all( - color: scheme.outlineVariant.withValues(alpha: 0.15), + color: scheme.outlineVariant.withValues(alpha: 0.06), width: 1, ), gradient: const LinearGradient( diff --git a/web/repertory/lib/widgets/mount_widget.dart b/web/repertory/lib/widgets/mount_widget.dart index 34df55de..1fe466dd 100644 --- a/web/repertory/lib/widgets/mount_widget.dart +++ b/web/repertory/lib/widgets/mount_widget.dart @@ -24,231 +24,255 @@ class _MountWidgetState extends State { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return Card( - margin: const EdgeInsets.all(0.0), - elevation: 12, - color: scheme.primary.withValues(alpha: constants.primaryAlpha), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(constants.borderRadiusSmall), - side: BorderSide( - color: scheme.outlineVariant.withValues(alpha: 0.06), - width: 1, - ), - ), - child: Container( - decoration: BoxDecoration( + final titleStyle = textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.15, + color: scheme.onSurface.withValues(alpha: 0.96), + ); + final subStyle = textTheme.bodyMedium?.copyWith( + color: scheme.onSurface.withValues(alpha: 0.78), + ); + final pathStyle = textTheme.bodyMedium?.copyWith( + color: scheme.onSurface.withValues(alpha: 0.68), + ); + + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 120), + child: Card( + margin: const EdgeInsets.all(0.0), + elevation: 12, + color: scheme.primary.withValues(alpha: constants.primaryAlpha), + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(constants.borderRadiusSmall), - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: constants.gradientColors2, + side: BorderSide( + color: scheme.outlineVariant.withValues(alpha: 0.06), + width: 1, ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.22), - blurRadius: constants.borderRadiusSmall, - offset: const Offset(0, constants.borderRadiusSmall), - ), - ], ), - child: Consumer( - builder: (context, Mount mount, _) { - final titleStyle = textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - letterSpacing: 0.15, - color: scheme.onSurface.withValues(alpha: 0.96), - ); - final subStyle = textTheme.bodyMedium?.copyWith( - color: scheme.onSurface.withValues(alpha: 0.78), - ); - final pathStyle = textTheme.bodySmall?.copyWith( - color: scheme.onSurface.withValues(alpha: 0.68), - ); - - final nameText = SelectableText( - formatMountName(mount.type, mount.name), - style: subStyle, - ); - - return ListTile( - isThreeLine: true, - contentPadding: const EdgeInsets.all(constants.paddingSmall), - leading: Container( - width: 46, - height: 46, + child: Stack( + children: [ + Positioned.fill( + child: Container( decoration: BoxDecoration( - color: scheme.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular( constants.borderRadiusSmall, ), - border: Border.all( - color: scheme.outlineVariant.withValues(alpha: 0.08), - width: 1, + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: constants.gradientColors2, ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.24), + color: Colors.black.withValues(alpha: 0.22), blurRadius: constants.borderRadiusSmall, - offset: const Offset(0, 5), + offset: const Offset(0, constants.borderRadiusSmall), ), ], ), - child: IconButton( - tooltip: 'Edit settings', - icon: Icon( - Icons.settings, - color: scheme.onSurface.withValues(alpha: 0.92), - size: 22, - ), - onPressed: () { - Navigator.pushNamed(context, '/edit', arguments: mount); - }, + ), + ), + Padding( + padding: const EdgeInsets.all(constants.padding), + child: Consumer( + builder: (context, Mount mount, _) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _GearBadge( + onTap: () { + Navigator.pushNamed( + context, + '/edit', + arguments: mount, + ); + }, + ), + const SizedBox(width: constants.padding), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + mount.provider, + style: titleStyle, + ), + SelectableText( + '${_formatType(mount.type)} • ${formatMountName(mount.type, mount.name)}', + style: subStyle, + ), + ], + ), + ), + _ToggleFramed( + mounted: mount.mounted, + onPressed: _createMountHandler(context, mount), + ), + ], + ), + const SizedBox(height: constants.padding), + Row( + children: [ + Expanded( + child: SelectableText( + _prettyPath(mount), + style: pathStyle, + ), + ), + const SizedBox(width: constants.padding), + _EditPathButton( + enabled: mount.mounted == false, + onPressed: () async { + if (!_editEnabled) { + return; + } + setState(() { + _editEnabled = false; + }); + + final available = await mount + .getAvailableLocations(); + + if (!mounted) { + setState(() { + _editEnabled = true; + }); + return; + } + + if (!context.mounted) { + return; + } + + final location = await editMountLocation( + context, + available, + location: mount.path, + ); + if (location != null) { + await mount.setMountLocation(location); + } + + if (mounted) { + setState(() { + _editEnabled = true; + }); + } + }, + ), + ], + ), + ], + ); + }, + ), + ), + Positioned( + left: constants.padding, + right: constants.padding, + bottom: constants.padding - 1, + child: Container( + height: 1, + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(1), ), ), - title: SelectableText(mount.provider, style: titleStyle), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - nameText, - const SizedBox(height: 3), - SelectableText( - mount.path.isEmpty && mount.mounted == null - ? 'loading...' - : mount.path.isEmpty - ? '' - : mount.path, - style: pathStyle, - ), - ], - ), - trailing: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (mount.mounted != null && !mount.mounted!) - IconButton( - icon: const Icon(Icons.edit), - color: scheme.onSurface.withValues(alpha: 0.70), - tooltip: 'Edit mount location', - onPressed: () async { - setState(() { - _editEnabled = false; - }); - final available = await mount.getAvailableLocations(); - if (context.mounted) { - final location = await editMountLocation( - context, - available, - location: mount.path, - ); - if (location != null) { - await mount.setMountLocation(location); - } - } - setState(() { - _editEnabled = true; - }); - }, - ), - IconButton( - iconSize: 32, - splashRadius: 26, - icon: Icon( - mount.mounted == null - ? Icons.hourglass_top - : mount.mounted! - ? Icons.toggle_on - : Icons.toggle_off, - ), - color: mount.mounted ?? false - ? scheme.primary - : scheme.outline.withValues(alpha: 0.70), - tooltip: mount.mounted == null - ? '' - : mount.mounted! - ? 'Unmount' - : 'Mount', - onPressed: _createMountHandler(context, mount), - ), - ], - ), - ); - }, + ), + ], ), ), ); } - VoidCallback? _createMountHandler(BuildContext context, Mount mount) { - return _enabled && mount.mounted != null - ? () async { - if (mount.mounted == null) { - return; - } - - final mounted = mount.mounted!; - - setState(() { - _enabled = false; - }); - - final location = await _getMountLocation(context, mount); - - void cleanup() { - setState(() { - _enabled = true; - }); - } - - if (!context.mounted && location == null) { - displayErrorMessage(context, "Mount location is not set"); - cleanup(); - return; - } - - final success = await mount.mount(mounted, location: location); - if (success || - mounted || - constants.navigatorKey.currentContext == null || - !constants.navigatorKey.currentContext!.mounted) { - cleanup(); - return; - } - - displayErrorMessage( - context, - "Mount location is not available: $location", - ); - cleanup(); - } - : null; + String _formatType(String type) { + if (type.toUpperCase() == 'S3') { + return 'S3'; + } + if (type.isEmpty) { + return type; + } + return type[0].toUpperCase() + type.substring(1).toLowerCase(); } - @override - void dispose() { - _timer?.cancel(); - _timer = null; - super.dispose(); + String _prettyPath(Mount mount) { + if (mount.path.isEmpty && mount.mounted == null) { + return 'loading...'; + } + if (mount.path.isEmpty) { + return ''; + } + return mount.path; + } + + VoidCallback? _createMountHandler(BuildContext context, Mount mount) { + if (!(_enabled && mount.mounted != null)) { + return null; + } + + return () async { + if (mount.mounted == null) { + return; + } + + final mounted = mount.mounted!; + setState(() { + _enabled = false; + }); + + final location = await _getMountLocation(context, mount); + + void cleanup() { + setState(() { + _enabled = true; + }); + } + + if (!mounted && location == null) { + if (!context.mounted) { + return; + } + displayErrorMessage(context, 'Mount location is not set'); + cleanup(); + return; + } + + final success = await mount.mount(mounted, location: location); + if (success || + mounted || + constants.navigatorKey.currentContext == null || + !constants.navigatorKey.currentContext!.mounted) { + cleanup(); + return; + } + + displayErrorMessage( + context, + 'Mount location is not available: $location', + ); + cleanup(); + }; } Future _getMountLocation(BuildContext context, Mount mount) async { if (mount.mounted ?? false) { return null; } - if (mount.path.isNotEmpty) { return mount.path; } - String? location = await mount.getMountLocation(); if (location != null) { return location; } - if (!context.mounted) { - return location; - } - + // ignore: use_build_context_synchronously return editMountLocation(context, await mount.getAvailableLocations()); } @@ -260,6 +284,13 @@ class _MountWidgetState extends State { }); } + @override + void dispose() { + _timer?.cancel(); + _timer = null; + super.dispose(); + } + @override void setState(VoidCallback fn) { if (!mounted) { @@ -268,3 +299,135 @@ class _MountWidgetState extends State { super.setState(fn); } } + +class _FramedBox extends StatelessWidget { + final IconData icon; + final VoidCallback? onTap; + final Color? iconColor; + final double iconSize; + + const _FramedBox({ + required this.icon, + this.onTap, + this.iconColor, + this.iconSize = 22, + }); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final radius = BorderRadius.circular(constants.borderRadiusSmall); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: radius, + child: Ink( + width: 46, + height: 46, + decoration: BoxDecoration( + color: scheme.primary.withValues(alpha: 0.12), + borderRadius: radius, + border: Border.all( + color: scheme.outlineVariant.withValues(alpha: 0.08), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.24), + blurRadius: constants.borderRadiusSmall / 2.0, + offset: const Offset(0, 5), + ), + ], + ), + child: Center( + child: Transform.scale( + scale: 0.90, + child: Icon( + icon, + color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92), + size: iconSize, + ), + ), + ), + ), + ), + ); + } +} + +class _GearBadge extends StatelessWidget { + final VoidCallback onTap; + const _GearBadge({required this.onTap}); + + @override + Widget build(BuildContext context) { + return _FramedBox(icon: Icons.settings, onTap: onTap, iconSize: 22); + } +} + +class _ToggleFramed extends StatelessWidget { + final bool? mounted; + final VoidCallback? onPressed; + + const _ToggleFramed({required this.mounted, required this.onPressed}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final bool isOn = mounted ?? false; + + IconData icon = Icons.hourglass_top; + Color iconColor = scheme.onSurface.withValues(alpha: 0.60); + + if (mounted != null) { + icon = isOn ? Icons.toggle_on : Icons.toggle_off; + iconColor = isOn + ? scheme.primary + : scheme.onSurface.withValues(alpha: 0.55); + } + + return _FramedBox( + icon: icon, + iconColor: iconColor, + onTap: mounted == null ? null : onPressed, + iconSize: 22, + ); + } +} + +class _EditPathButton extends StatelessWidget { + final bool enabled; + final VoidCallback onPressed; + + const _EditPathButton({required this.enabled, required this.onPressed}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + + return Opacity( + opacity: enabled ? 1.0 : 0.45, + child: OutlinedButton.icon( + onPressed: enabled ? onPressed : null, + icon: const Icon(Icons.edit, size: 18), + label: const Text('Edit path'), + style: OutlinedButton.styleFrom( + foregroundColor: scheme.primary, + side: BorderSide( + color: scheme.primary.withValues(alpha: 0.55), + width: 1.2, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + constants.borderRadiusSmall * 1.6, + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + backgroundColor: scheme.primary.withValues(alpha: 0.10), + ), + ), + ); + } +} diff --git a/web/repertory/web/index.html b/web/repertory/web/index.html index b77560f8..3c754a5b 100644 --- a/web/repertory/web/index.html +++ b/web/repertory/web/index.html @@ -31,6 +31,11 @@ +