From 2d184295a7b80caee0c70f89223118ecc049c764 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 25 Mar 2026 00:27:59 +1100 Subject: [PATCH 1/5] Add the security key status into the navigation drawer menu --- lib/src/widgets/solid_nav_drawer.dart | 126 +++++++++++++++--- lib/src/widgets/solid_nav_drawer_header.dart | 36 +++-- .../widgets/solid_scaffold_build_helper.dart | 21 +++ .../solid_scaffold_widget_builder.dart | 26 ++++ 4 files changed, 182 insertions(+), 27 deletions(-) diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart index 53ea331..99edccc 100644 --- a/lib/src/widgets/solid_nav_drawer.dart +++ b/lib/src/widgets/solid_nav_drawer.dart @@ -33,10 +33,16 @@ library; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:solidpod/solidpod.dart' show isUserLoggedIn; import 'package:solidui/src/constants/navigation.dart'; +import 'package:solidui/src/handlers/solid_auth_handler.dart'; +import 'package:solidui/src/utils/solid_notifications.dart'; import 'package:solidui/src/widgets/solid_nav_drawer_header.dart'; import 'package:solidui/src/widgets/solid_nav_models.dart'; +import 'package:solidui/src/widgets/solid_security_key_cache_dialogs.dart'; +import 'package:solidui/src/widgets/solid_security_key_manager.dart'; +import 'package:solidui/src/widgets/solid_status_bar_models.dart'; /// A solid navigation drawer component. @@ -73,6 +79,14 @@ class SolidNavDrawer extends StatefulWidget { final bool showLogout; + /// Callback when the user name area is tapped (for login/logout). + + final void Function(BuildContext)? onUserNameTap; + + /// Security key status to display above the logout option. + + final SolidSecurityKeyStatus? securityKeyStatus; + /// Optional additional menu items to display after the main tabs. final List? additionalMenuItems; @@ -91,6 +105,8 @@ class SolidNavDrawer extends StatefulWidget { this.logoutIcon, this.logoutText, this.showLogout = true, + this.onUserNameTap, + this.securityKeyStatus, this.additionalMenuItems, this.drawerShape, }); @@ -163,6 +179,12 @@ class _SolidNavDrawerState extends State { isVersionLoaded: _isVersionLoaded, appVersion: _appVersion, getVersionToDisplay: _getVersionToDisplay, + onUserNameTap: widget.onUserNameTap != null + ? () { + Navigator.of(context).pop(); + widget.onUserNameTap!(context); + } + : null, ), Container( padding: const EdgeInsets.all(NavigationConstants.navDrawerPadding), @@ -175,8 +197,8 @@ class _SolidNavDrawerState extends State { }), if (widget.additionalMenuItems != null) ...widget.additionalMenuItems!, - if (widget.showLogout && widget.onLogout != null) - ..._buildLogoutSection(context, theme), + if (widget.securityKeyStatus != null || _canLogout()) + ..._buildBottomSection(context, theme), ], ), ), @@ -217,30 +239,96 @@ class _SolidNavDrawerState extends State { ); } - List _buildLogoutSection(BuildContext context, ThemeData theme) { + List _buildBottomSection(BuildContext context, ThemeData theme) { return [ Divider( height: NavigationConstants.navDividerHeight, color: theme.dividerColor, ), - ListTile( - leading: Icon( - widget.logoutIcon ?? Icons.logout, - color: _canLogout() ? theme.colorScheme.error : theme.disabledColor, - ), - title: Text( - widget.logoutText ?? 'Logout', - style: TextStyle( - color: _canLogout() ? theme.colorScheme.error : theme.disabledColor, + if (widget.securityKeyStatus != null) + _buildSecurityKeyTile(context, theme), + if (_canLogout()) + ListTile( + leading: Icon( + widget.logoutIcon ?? Icons.logout, + color: theme.colorScheme.error, + ), + title: Text( + widget.logoutText ?? 'Logout', + style: TextStyle(color: theme.colorScheme.error), ), + onTap: () { + Navigator.of(context).pop(); + widget.onLogout!(context); + }, ), - onTap: _canLogout() - ? () { - Navigator.of(context).pop(); - widget.onLogout!(context); - } - : null, - ), ]; } + + Widget _buildSecurityKeyTile(BuildContext context, ThemeData theme) { + final status = widget.securityKeyStatus!; + final isKeySaved = status.isKeySaved == true; + + return ListTile( + leading: Icon( + isKeySaved ? Icons.key : Icons.key_off, + color: + isKeySaved ? theme.colorScheme.tertiary : theme.colorScheme.error, + ), + title: Text( + status.displayText, + style: TextStyle( + color: + isKeySaved ? theme.colorScheme.tertiary : theme.colorScheme.error, + ), + ), + onTap: () { + Navigator.of(context).pop(); + if (status.onTap != null) { + status.onTap!(); + } else { + _showSecurityKeyManager(context, status); + } + }, + ); + } + + Future _showSecurityKeyManager( + BuildContext context, + SolidSecurityKeyStatus config, + ) async { + final isLoggedIn = await isUserLoggedIn(); + if (!context.mounted) return; + + if (!isLoggedIn) { + final shouldLogin = + await SecurityKeyCacheDialogs.showLoginRequiredDialog(context); + if (shouldLogin && context.mounted) { + await SolidAuthHandler.instance.handleLogin(context); + } + return; + } + + if (!context.mounted) return; + showDialog( + context: context, + barrierColor: Colors.black.withValues(alpha: 0.5), + builder: (BuildContext dialogContext) => SolidSecurityKeyManager( + config: SolidSecurityKeyManagerConfig( + appWidget: config.appWidget ?? const SizedBox(), + title: config.title ?? 'Security Key Management', + ), + onKeyStatusChanged: (bool hasKey) { + config.onKeyStatusChanged?.call(hasKey); + try { + SecurityKeyStatusChangedNotification( + isKeySaved: hasKey, + ).dispatch(dialogContext); + } catch (e) { + debugPrint('Could not refresh security key status: $e'); + } + }, + ), + ); + } } diff --git a/lib/src/widgets/solid_nav_drawer_header.dart b/lib/src/widgets/solid_nav_drawer_header.dart index 21c0e3f..8379e29 100644 --- a/lib/src/widgets/solid_nav_drawer_header.dart +++ b/lib/src/widgets/solid_nav_drawer_header.dart @@ -49,6 +49,7 @@ class SolidNavDrawerHeader { required bool isVersionLoaded, required String? appVersion, required String Function() getVersionToDisplay, + VoidCallback? onUserNameTap, }) { final bool willShowVersion = user.versionConfig != null; final double bottomPadding = @@ -70,14 +71,7 @@ class SolidNavDrawerHeader { color: theme.colorScheme.onPrimaryContainer, ), const Gap(NavigationConstants.userInfoSpacing), - Text( - user.effectiveUserName, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontSize: NavigationConstants.userNameFontSize, - fontWeight: FontWeight.w600, - ), - ), + _buildUserName(theme, user, onUserNameTap), if (user.showWebId && user.webId != null && user.webId!.isNotEmpty) ..._buildWebIdSection(context, theme, user), if (user.versionConfig != null) @@ -94,6 +88,32 @@ class SolidNavDrawerHeader { ); } + static Widget _buildUserName( + ThemeData theme, + SolidNavUserInfo user, + VoidCallback? onUserNameTap, + ) { + final text = Text( + user.effectiveUserName, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: NavigationConstants.userNameFontSize, + fontWeight: FontWeight.w600, + ), + ); + + if (onUserNameTap == null) return text; + + return InkWell( + onTap: onUserNameTap, + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: text, + ), + ); + } + static List _buildWebIdSection( BuildContext context, ThemeData theme, diff --git a/lib/src/widgets/solid_scaffold_build_helper.dart b/lib/src/widgets/solid_scaffold_build_helper.dart index b878012..47ffd61 100644 --- a/lib/src/widgets/solid_scaffold_build_helper.dart +++ b/lib/src/widgets/solid_scaffold_build_helper.dart @@ -31,11 +31,13 @@ library; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:solidui/src/handlers/solid_auth_handler.dart'; import 'package:solidui/src/widgets/solid_about_models.dart'; import 'package:solidui/src/widgets/solid_nav_drawer.dart'; import 'package:solidui/src/widgets/solid_scaffold_helpers.dart'; import 'package:solidui/src/widgets/solid_scaffold_layout_builder.dart'; import 'package:solidui/src/widgets/solid_scaffold_models.dart'; +import 'package:solidui/src/widgets/solid_status_bar_models.dart'; import 'package:solidui/src/widgets/solid_theme_notifier.dart'; /// Helper for building the main Scaffold in SolidScaffold. @@ -168,6 +170,22 @@ class SolidScaffoldBuildHelper { ), buildDrawer: () { if (isWideScreen || config.menu == null) return null; + + SolidSecurityKeyStatus? drawerSecurityKeyStatus; + final original = config.statusBar?.securityKeyStatus; + if (original != null) { + drawerSecurityKeyStatus = SolidSecurityKeyStatus( + isKeySaved: isKeySaved, + onTap: original.onTap, + onKeyStatusChanged: original.onKeyStatusChanged, + title: original.title, + appWidget: original.appWidget, + keySavedText: original.keySavedText, + keyNotSavedText: original.keyNotSavedText, + tooltip: original.tooltip, + ); + } + return SolidNavDrawer( userInfo: config.userInfo, tabs: SolidScaffoldHelpers.convertToNavTabs(config.menu), @@ -175,6 +193,9 @@ class SolidScaffoldBuildHelper { onTabSelected: onMenuSelected, onLogout: config.onLogout, showLogout: config.onLogout != null, + onUserNameTap: (drawerContext) => + SolidAuthHandler.instance.handleAuthAction(drawerContext), + securityKeyStatus: drawerSecurityKeyStatus, ); }, endDrawer: config.endDrawer, diff --git a/lib/src/widgets/solid_scaffold_widget_builder.dart b/lib/src/widgets/solid_scaffold_widget_builder.dart index 1fe7559..ef777a6 100644 --- a/lib/src/widgets/solid_scaffold_widget_builder.dart +++ b/lib/src/widgets/solid_scaffold_widget_builder.dart @@ -38,6 +38,7 @@ import 'package:solidui/src/widgets/solid_scaffold.dart'; import 'package:solidui/src/widgets/solid_scaffold_build_helper.dart'; import 'package:solidui/src/widgets/solid_scaffold_helpers.dart'; import 'package:solidui/src/widgets/solid_scaffold_layout_builder.dart'; +import 'package:solidui/src/widgets/solid_status_bar_models.dart'; import 'package:solidui/src/widgets/solid_theme_notifier.dart'; /// Widget builder specifically for SolidScaffold. @@ -87,6 +88,28 @@ class SolidScaffoldWidgetBuilder { ); } + /// Builds an effective security key status for the drawer with the current + /// [isKeySaved] value from scaffold state. + + static SolidSecurityKeyStatus? _buildDrawerSecurityKeyStatus( + SolidScaffold widget, + bool isKeySaved, + ) { + final original = widget.statusBar?.securityKeyStatus; + if (original == null) return null; + + return SolidSecurityKeyStatus( + isKeySaved: isKeySaved, + onTap: original.onTap, + onKeyStatusChanged: original.onKeyStatusChanged, + title: original.title, + appWidget: original.appWidget, + keySavedText: original.keySavedText, + keyNotSavedText: original.keyNotSavedText, + tooltip: original.tooltip, + ); + } + /// Builds scaffold directly from widget parameters. static Widget buildFromWidget({ @@ -159,6 +182,9 @@ class SolidScaffoldWidgetBuilder { onTabSelected: onMenuSelected, onLogout: effectiveLogout, showLogout: effectiveLogout != null, + onUserNameTap: (drawerContext) => + SolidAuthHandler.instance.handleAuthAction(drawerContext), + securityKeyStatus: _buildDrawerSecurityKeyStatus(widget, isKeySaved), ); }, endDrawer: widget.endDrawer, From 2f7aa10ca29668b4b3344c7d4d31263473cd97cd Mon Sep 17 00:00:00 2001 From: Graham Williams Date: Wed, 25 Mar 2026 09:03:19 +1100 Subject: [PATCH 2/5] Remove colon in Security Key --- lib/src/widgets/solid_status_bar_models.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/widgets/solid_status_bar_models.dart b/lib/src/widgets/solid_status_bar_models.dart index dfb5b29..6be7212 100644 --- a/lib/src/widgets/solid_status_bar_models.dart +++ b/lib/src/widgets/solid_status_bar_models.dart @@ -1,6 +1,6 @@ /// Solid Status Bar Models. /// -// Time-stamp: +// Time-stamp: /// /// Copyright (C) 2025, Software Innovation Institute, ANU. /// @@ -368,9 +368,9 @@ class SolidSecurityKeyStatus { String get displayText { if (isKeySaved == true) { - return keySavedText ?? 'Security Key: Cached Locally'; + return keySavedText ?? 'Security Key Cached Locally'; } else { - return keyNotSavedText ?? 'Security Key: Not Cached'; + return keyNotSavedText ?? 'Security Key Not Cached'; } } From df3fcb320a01caa2a27ff8562a9217ac53966372 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 25 Mar 2026 16:30:24 +1100 Subject: [PATCH 3/5] Optimise the layout --- lib/src/widgets/solid_nav_drawer.dart | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart index 99edccc..935a5e8 100644 --- a/lib/src/widgets/solid_nav_drawer.dart +++ b/lib/src/widgets/solid_nav_drawer.dart @@ -154,8 +154,6 @@ class _SolidNavDrawerState extends State { return '0.0.0+0'; } - bool _canLogout() => widget.showLogout && widget.onLogout != null; - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -197,7 +195,8 @@ class _SolidNavDrawerState extends State { }), if (widget.additionalMenuItems != null) ...widget.additionalMenuItems!, - if (widget.securityKeyStatus != null || _canLogout()) + if (widget.securityKeyStatus != null || + widget.onUserNameTap != null) ..._buildBottomSection(context, theme), ], ), @@ -239,6 +238,9 @@ class _SolidNavDrawerState extends State { ); } + bool get _isLoggedIn => + widget.userInfo?.webId != null && widget.userInfo!.webId!.isNotEmpty; + List _buildBottomSection(BuildContext context, ThemeData theme) { return [ Divider( @@ -247,21 +249,8 @@ class _SolidNavDrawerState extends State { ), if (widget.securityKeyStatus != null) _buildSecurityKeyTile(context, theme), - if (_canLogout()) - ListTile( - leading: Icon( - widget.logoutIcon ?? Icons.logout, - color: theme.colorScheme.error, - ), - title: Text( - widget.logoutText ?? 'Logout', - style: TextStyle(color: theme.colorScheme.error), - ), - onTap: () { - Navigator.of(context).pop(); - widget.onLogout!(context); - }, - ), + if (widget.onUserNameTap != null) + _buildLoginStatusTile(context, theme), ]; } @@ -270,16 +259,10 @@ class _SolidNavDrawerState extends State { final isKeySaved = status.isKeySaved == true; return ListTile( - leading: Icon( - isKeySaved ? Icons.key : Icons.key_off, - color: - isKeySaved ? theme.colorScheme.tertiary : theme.colorScheme.error, - ), title: Text( status.displayText, style: TextStyle( - color: - isKeySaved ? theme.colorScheme.tertiary : theme.colorScheme.error, + color: isKeySaved ? null : theme.colorScheme.primary, ), ), onTap: () { @@ -293,6 +276,23 @@ class _SolidNavDrawerState extends State { ); } + Widget _buildLoginStatusTile(BuildContext context, ThemeData theme) { + final statusText = _isLoggedIn ? 'Logged In' : 'Not Logged In'; + + return ListTile( + title: Text( + 'Login Status: $statusText', + style: TextStyle( + color: _isLoggedIn ? null : theme.colorScheme.primary, + ), + ), + onTap: () { + Navigator.of(context).pop(); + widget.onUserNameTap!(context); + }, + ); + } + Future _showSecurityKeyManager( BuildContext context, SolidSecurityKeyStatus config, From f97300b5288c7336bb82e786aad269ee983f18d0 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 25 Mar 2026 16:37:42 +1100 Subject: [PATCH 4/5] Lint --- lib/src/widgets/solid_nav_drawer.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart index 935a5e8..c2e39a5 100644 --- a/lib/src/widgets/solid_nav_drawer.dart +++ b/lib/src/widgets/solid_nav_drawer.dart @@ -249,8 +249,7 @@ class _SolidNavDrawerState extends State { ), if (widget.securityKeyStatus != null) _buildSecurityKeyTile(context, theme), - if (widget.onUserNameTap != null) - _buildLoginStatusTile(context, theme), + if (widget.onUserNameTap != null) _buildLoginStatusTile(context, theme), ]; } From bba67f9dbb45b4f64486e1f61cc4f0eaf511b0ec Mon Sep 17 00:00:00 2001 From: Graham Williams Date: Thu, 26 Mar 2026 09:24:24 +1100 Subject: [PATCH 5/5] Shorten login status. --- lib/src/widgets/solid_nav_drawer.dart | 4 ++-- lib/src/widgets/solid_status_bar.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart index c2e39a5..0344d13 100644 --- a/lib/src/widgets/solid_nav_drawer.dart +++ b/lib/src/widgets/solid_nav_drawer.dart @@ -1,6 +1,6 @@ /// Solid Navigation Drawer. /// -// Time-stamp: +// Time-stamp: /// /// Copyright (C) 2025, Software Innovation Institute, ANU. /// @@ -280,7 +280,7 @@ class _SolidNavDrawerState extends State { return ListTile( title: Text( - 'Login Status: $statusText', + statusText, style: TextStyle( color: _isLoggedIn ? null : theme.colorScheme.primary, ), diff --git a/lib/src/widgets/solid_status_bar.dart b/lib/src/widgets/solid_status_bar.dart index 8bfeb62..b3d05d5 100644 --- a/lib/src/widgets/solid_status_bar.dart +++ b/lib/src/widgets/solid_status_bar.dart @@ -1,6 +1,6 @@ /// Solid Status Bar. /// -// Time-stamp: +// Time-stamp: /// /// Copyright (C) 2025, Software Innovation Institute, ANU. /// @@ -141,7 +141,7 @@ class SolidStatusBar extends StatelessWidget { message: loginStatus.tooltipText, child: _createInteractiveText( context: context, - text: 'Login Status: ${loginStatus.displayText}', + text: loginStatus.displayText, onTap: loginStatus.onTap ?? () => SolidAuthHandler.instance.handleAuthAction(context), style: theme.textTheme.bodyMedium?.copyWith(