diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart index 53ea331..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. /// @@ -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, }); @@ -138,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); @@ -163,6 +177,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 +195,9 @@ class _SolidNavDrawerState extends State { }), if (widget.additionalMenuItems != null) ...widget.additionalMenuItems!, - if (widget.showLogout && widget.onLogout != null) - ..._buildLogoutSection(context, theme), + if (widget.securityKeyStatus != null || + widget.onUserNameTap != null) + ..._buildBottomSection(context, theme), ], ), ), @@ -217,30 +238,96 @@ class _SolidNavDrawerState extends State { ); } - List _buildLogoutSection(BuildContext context, ThemeData theme) { + bool get _isLoggedIn => + widget.userInfo?.webId != null && widget.userInfo!.webId!.isNotEmpty; + + 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, + if (widget.securityKeyStatus != null) + _buildSecurityKeyTile(context, theme), + if (widget.onUserNameTap != null) _buildLoginStatusTile(context, theme), + ]; + } + + Widget _buildSecurityKeyTile(BuildContext context, ThemeData theme) { + final status = widget.securityKeyStatus!; + final isKeySaved = status.isKeySaved == true; + + return ListTile( + title: Text( + status.displayText, + style: TextStyle( + color: isKeySaved ? null : theme.colorScheme.primary, ), - title: Text( - widget.logoutText ?? 'Logout', - style: TextStyle( - color: _canLogout() ? theme.colorScheme.error : theme.disabledColor, - ), + ), + onTap: () { + Navigator.of(context).pop(); + if (status.onTap != null) { + status.onTap!(); + } else { + _showSecurityKeyManager(context, status); + } + }, + ); + } + + Widget _buildLoginStatusTile(BuildContext context, ThemeData theme) { + final statusText = _isLoggedIn ? 'Logged In' : 'Not Logged In'; + + return ListTile( + title: Text( + statusText, + style: TextStyle( + color: _isLoggedIn ? null : theme.colorScheme.primary, ), - onTap: _canLogout() - ? () { - Navigator.of(context).pop(); - widget.onLogout!(context); - } - : null, ), - ]; + onTap: () { + Navigator.of(context).pop(); + widget.onUserNameTap!(context); + }, + ); + } + + 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, 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( 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'; } }