Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 109 additions & 22 deletions lib/src/widgets/solid_nav_drawer.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Solid Navigation Drawer.
///
// Time-stamp: <Friday 2025-10-17 10:33:35 +1100 Graham Williams>
// Time-stamp: <Thursday 2026-03-26 09:22:09 +1100 Graham Williams>
///
/// Copyright (C) 2025, Software Innovation Institute, ANU.
///
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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<Widget>? additionalMenuItems;
Expand All @@ -91,6 +105,8 @@ class SolidNavDrawer extends StatefulWidget {
this.logoutIcon,
this.logoutText,
this.showLogout = true,
this.onUserNameTap,
this.securityKeyStatus,
this.additionalMenuItems,
this.drawerShape,
});
Expand Down Expand Up @@ -138,8 +154,6 @@ class _SolidNavDrawerState extends State<SolidNavDrawer> {
return '0.0.0+0';
}

bool _canLogout() => widget.showLogout && widget.onLogout != null;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Expand All @@ -163,6 +177,12 @@ class _SolidNavDrawerState extends State<SolidNavDrawer> {
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),
Expand All @@ -175,8 +195,9 @@ class _SolidNavDrawerState extends State<SolidNavDrawer> {
}),
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),
],
),
),
Expand Down Expand Up @@ -217,30 +238,96 @@ class _SolidNavDrawerState extends State<SolidNavDrawer> {
);
}

List<Widget> _buildLogoutSection(BuildContext context, ThemeData theme) {
bool get _isLoggedIn =>
widget.userInfo?.webId != null && widget.userInfo!.webId!.isNotEmpty;

List<Widget> _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<void> _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');
}
},
),
);
}
}
36 changes: 28 additions & 8 deletions lib/src/widgets/solid_nav_drawer_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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)
Expand All @@ -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<Widget> _buildWebIdSection(
BuildContext context,
ThemeData theme,
Expand Down
21 changes: 21 additions & 0 deletions lib/src/widgets/solid_scaffold_build_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -168,13 +170,32 @@ 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),
selectedIndex: currentSelectedIndex,
onTabSelected: onMenuSelected,
onLogout: config.onLogout,
showLogout: config.onLogout != null,
onUserNameTap: (drawerContext) =>
SolidAuthHandler.instance.handleAuthAction(drawerContext),
securityKeyStatus: drawerSecurityKeyStatus,
);
},
endDrawer: config.endDrawer,
Expand Down
26 changes: 26 additions & 0 deletions lib/src/widgets/solid_scaffold_widget_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions lib/src/widgets/solid_status_bar.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Solid Status Bar.
///
// Time-stamp: <Monday 2025-08-11 15:30:00 +1000 Tony Chen>
// Time-stamp: <Thursday 2026-03-26 09:23:47 +1100 Graham Williams>
///
/// Copyright (C) 2025, Software Innovation Institute, ANU.
///
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions lib/src/widgets/solid_status_bar_models.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Solid Status Bar Models.
///
// Time-stamp: <Sunday 2025-10-26 13:36:17 +1100 Graham Williams>
// Time-stamp: <Wednesday 2026-03-25 08:57:39 +1100 Graham Williams>
///
/// Copyright (C) 2025, Software Innovation Institute, ANU.
///
Expand Down Expand Up @@ -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';
}
}

Expand Down
Loading