From 8e8ba74dc76b71bb52b1c91caabd1eab67e71b07 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 30 Jan 2026 19:44:22 +1100 Subject: [PATCH 1/4] Migrate security key related elements from SolidPod --- lib/solidui.dart | 6 + lib/src/constants/ui.dart | 297 +++++++++++++++++- lib/src/utils/snack_bar.dart | 51 ++++ lib/src/widgets/change_key_dialog.dart | 170 +++++++++++ lib/src/widgets/secret_text_field.dart | 120 ++++++++ lib/src/widgets/security_key_ui.dart | 401 +++++++++++++++++++++++++ 6 files changed, 1042 insertions(+), 3 deletions(-) create mode 100644 lib/src/utils/snack_bar.dart create mode 100644 lib/src/widgets/change_key_dialog.dart create mode 100644 lib/src/widgets/secret_text_field.dart create mode 100644 lib/src/widgets/security_key_ui.dart diff --git a/lib/solidui.dart b/lib/solidui.dart index ff611ab..91d8cf4 100644 --- a/lib/solidui.dart +++ b/lib/solidui.dart @@ -76,6 +76,12 @@ export 'src/widgets/solid_security_key_central_manager.dart'; export 'src/services/solid_security_key_service.dart'; export 'src/services/solid_security_key_notifier.dart'; +export 'src/widgets/secret_text_field.dart'; +export 'src/widgets/security_key_ui.dart'; +export 'src/widgets/change_key_dialog.dart'; + +export 'src/utils/snack_bar.dart'; + export 'src/widgets/solid_file.dart'; export 'src/widgets/solid_file_browser.dart'; export 'src/widgets/solid_file_uploader.dart'; diff --git a/lib/src/constants/ui.dart b/lib/src/constants/ui.dart index ae0ce6f..66013c2 100644 --- a/lib/src/constants/ui.dart +++ b/lib/src/constants/ui.dart @@ -24,12 +24,123 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. /// -/// Authors: Ashley Tang, Jess Moore +/// Authors: Ashley Tang, Jess Moore, Tony Chen library; import 'package:flutter/material.dart'; +/// Thresholds for window size. + +class WindowSize { + /// Small width threshold. + + static const double smallWidthLimit = 600; + + /// Small height threshold. + + static const double smallHeightLimit = 600; + + /// Boolean describing whether the parent widget is narrow. + /// Derived from the box constraints found by LayoutBuilder(). + /// + /// Arguments: + /// - [constraints] - The box constraints of the parent widget + /// where LayoutBuilder() was called. + + bool isNarrowWindow(BoxConstraints constraints) { + final bool isNarrow; + if (constraints.maxWidth < WindowSize.smallWidthLimit) { + isNarrow = true; + } else { + isNarrow = false; + } + + return isNarrow; + } +} + +/// Approximate size for grid items used for displaying text in list item. + +class ListItemSize { + /// Approximate height of compressed item in list when list item text + /// is line wrapped in a narrow mobile phone size window. + /// (Where each of note title, created date time, modified date time + /// are line wrapped to two lines.) + + static const double compressedItemHeight = 260; + + /// Approximate height of uncompressed item in list when list item text + /// is not line wrapped. + + static const double uncompressedItemHeight = 138; + + /// Calculate card aspect ratio to use for gridview builder cards + /// using the box constraints found by LayoutBuilder(). + /// + /// Arguments: + /// - [constraints] - The box constraints of the parent widget + /// where LayoutBuilder() was called. + + double calculateCardAspectRatio(BoxConstraints constraints) { + /// Aspect ratio (width / height) for gridview cards to display note items. + final double cardAspectRatio; + + // Derive card aspect ratio (width / height). + if (constraints.maxWidth < WindowSize.smallWidthLimit) { + cardAspectRatio = constraints.maxWidth / compressedItemHeight; + } else { + cardAspectRatio = constraints.maxWidth / uncompressedItemHeight; + } + return cardAspectRatio; + } +} + +/// Class for icon sizing in list items. + +class ListIconSize { + /// Icon width. + + static const double width = 50; + + /// Icon height. + + static const double height = 50; + + /// Width for two icons side by side. + + static const double twoIconWidth = (width * 2) + gap; + + /// Gap between icons. + + static const double gap = 15; +} + +/// Icon shape decoration for list items. + +ShapeDecoration listIconShape = + const ShapeDecoration(color: Colors.grey, shape: CircleBorder()); + +// Standard colours for actions and results. + +class ActionColors { + /// Green colour used for success. + + static const success = Colors.green; + + // Red colour used for error/failure. + + static const error = Colors.red; + + // Colour used for warning. + + static const warning = Color.fromARGB(255, 204, 99, 1); + + // Red colour used for delete action. + + static const delete = Colors.red; +} + /// Colours used across security dialogs and prompts. class SecurityColors { @@ -37,21 +148,115 @@ class SecurityColors { static const primary = Color(0xFF2E7D32); + /// Primary colour for dark mode (lighter green for better contrast). + + static const primaryDark = Color(0xFF66BB6A); + /// Accent colour (Lighter Green) used for dividers and secondary elements. static const accent = Color(0xFF4CAF50); + /// Accent colour for dark mode. + + static const accentDark = Color(0xFF81C784); + /// Background colour (Light Grey) used for dialog backgrounds. static const background = Color(0xFFF5F5F5); + /// Background colour for dark mode. + + static const backgroundDark = Color(0xFF1E1E1E); + /// Text colour (Dark Grey) used for main text content. static const text = Color(0xFF212121); + /// Text colour for dark mode. + + static const textDark = Color(0xFFE0E0E0); + /// Grey colour used for labels and secondary text. static const labelGrey = Colors.grey; + + /// Label colour for dark mode. + + static const labelGreyDark = Color(0xFF9E9E9E); + + /// Card background colour for light mode. + + static const cardBackground = Colors.white; + + /// Card background colour for dark mode. + + static const cardBackgroundDark = Color(0xFF2D2D2D); + + /// Separator colour for light mode. + + static const separator = Color(0xFFE0E0E0); + + /// Separator colour for dark mode. + + static const separatorDark = Color(0xFF424242); +} + +/// Helper class to obtain theme-aware colours for security UI components. +/// +/// This class provides methods that return appropriate colours based on the +/// current theme brightness (light or dark mode). + +class SecurityThemeColors { + /// Returns the primary colour based on the current theme. + + static Color primary(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.primaryDark : SecurityColors.primary; + } + + /// Returns the accent colour based on the current theme. + + static Color accent(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.accentDark : SecurityColors.accent; + } + + /// Returns the background colour based on the current theme. + + static Color background(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.backgroundDark : SecurityColors.background; + } + + /// Returns the text colour based on the current theme. + + static Color text(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.textDark : SecurityColors.text; + } + + /// Returns the label grey colour based on the current theme. + + static Color labelGrey(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.labelGreyDark : SecurityColors.labelGrey; + } + + /// Returns the card background colour based on the current theme. + + static Color cardBackground(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark + ? SecurityColors.cardBackgroundDark + : SecurityColors.cardBackground; + } + + /// Returns the separator colour based on the current theme. + + static Color separator(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.separatorDark : SecurityColors.separator; + } } /// Text styles used across security dialogs and prompts. @@ -82,6 +287,64 @@ class SecurityTextStyles { static const button = TextStyle(fontSize: 14, color: Colors.white); } +/// Helper class to obtain theme-aware text styles for security UI components. + +class SecurityThemeTextStyles { + /// Returns the heading style based on the current theme. + + static TextStyle heading(BuildContext context) { + return TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: SecurityThemeColors.primary(context), + ); + } + + /// Returns the body text style based on the current theme. + + static TextStyle body(BuildContext context) { + return TextStyle( + fontSize: 15, + color: SecurityThemeColors.text(context), + ); + } + + /// Returns the WebID style based on the current theme. + + static TextStyle webId(BuildContext context, {bool isLoggedIn = true}) { + return TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isLoggedIn ? SecurityThemeColors.primary(context) : Colors.red, + ); + } + + /// Returns the label style based on the current theme. + + static TextStyle label(BuildContext context) { + return TextStyle( + fontSize: 13, + color: SecurityThemeColors.labelGrey(context), + ); + } + + /// Returns the button text style. + + static const button = TextStyle( + fontSize: 14, + color: Colors.white, + ); + + /// Returns the cancel button style based on the current theme. + + static TextStyle cancelButton(BuildContext context) { + return TextStyle( + fontSize: 14, + color: SecurityThemeColors.text(context), + ); + } +} + /// Layout constants used across security dialogs and prompts. class SecurityLayout { @@ -201,10 +464,38 @@ class DropdownColors { static const accent = Color(0xFF4CAF50); } -/// Layout constants used for WebId dialogs +/// Layout constants used for WebId dialogs. class WebIdLayout { - /// Vertical gap between paragraphs + /// Vertical gap between paragraphs. + + static const paraVertGap = SizedBox(height: 10); + + /// Standard padding for dialog content. + + static const contentPadding = EdgeInsets.symmetric(horizontal: 50); + + /// Standard width for security dialogs. + + static const dialogWidth = 480.0; + + /// Height of dropdown suggestion box. + + static const dropdownHeight = 120.0; + + /// Elevation of dropdown suggestion cards. + + static double dropdownElevation = 5; + + /// Padding of dropdown suggestion list. + + static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); +} + +/// Layout constants used for Grant Permission Form Dialog. + +class GrantPermFormLayout { + /// Vertical gap between paragraphs. static const paraVertGap = SizedBox(height: 10); diff --git a/lib/src/utils/snack_bar.dart b/lib/src/utils/snack_bar.dart new file mode 100644 index 0000000..9d7aad3 --- /dev/null +++ b/lib/src/utils/snack_bar.dart @@ -0,0 +1,51 @@ +/// A utility function for displaying snack bars. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Dawei Chen, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +/// Displays a snack bar with a custom message. +/// +/// A customised background colour and duration can be used. + +void showSnackBar( + BuildContext context, + String msg, + Color bgColor, { + Duration duration = const Duration(seconds: 4), +}) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(msg), + backgroundColor: bgColor, + duration: duration, + ), + ); +} diff --git a/lib/src/widgets/change_key_dialog.dart b/lib/src/widgets/change_key_dialog.dart new file mode 100644 index 0000000..65855d0 --- /dev/null +++ b/lib/src/widgets/change_key_dialog.dart @@ -0,0 +1,170 @@ +/// Show a pop up widget to change the security key. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Kevin Wang, Dawei Chen + +library; + +import 'package:flutter/material.dart'; + +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +import 'package:solidpod/solidpod.dart' + show + NotLoggedInException, + verifySecurityKey, + KeyManager, + isUserLoggedIn, + getWebId; + +import 'package:solidui/src/utils/snack_bar.dart'; +import 'package:solidui/src/widgets/security_key_ui.dart'; + +/// Displays a dialog for changing the security key. +/// +/// [context] is the BuildContext from which this function is called. + +Future changeKeyPopup(BuildContext context, Widget child) async { + if (!await isUserLoggedIn()) { + throw NotLoggedInException( + 'User must be logged in to change security key.', + ); + } else { + final verificationKey = await KeyManager.getVerificationKey(); + final webId = await getWebId(); + + const message = + 'Please enter the current security key, the new security key,' + ' and repeat the new security key.'; + const currentKeyStr = 'current_security_key'; + const newKeyStr = 'new_security_key'; + const newKeyRepeatStr = 'new_security_key_repeat'; + final formKey = GlobalKey(); + + String? validateCurrentKey(String key) => + verifySecurityKey(key, verificationKey) + ? null + : 'Incorrect security key.'; + + String? validateNewKey(String key) => + verifySecurityKey(key, verificationKey) + ? 'New security key is identical to current security key.' + : null; + + String? validateNewKeyRepeat(String key) { + final formData = formKey.currentState?.value as Map; + if (formData.containsKey(newKeyStr) && + formData.containsKey(newKeyRepeatStr) && + formData[newKeyStr].toString() != + formData[newKeyRepeatStr].toString()) { + return 'New security keys do not match.'; + } + return null; + } + + final outerContext = context; + + Future submitForm(Map formDataMap) async { + final currentKey = formDataMap[currentKeyStr].toString(); + final newKey = formDataMap[newKeyStr].toString(); + final newKeyRepeat = formDataMap[newKeyRepeatStr].toString(); + + if (validateCurrentKey(currentKey) != null || + validateNewKey(newKey) != null || + validateNewKeyRepeat(newKeyRepeat) != null) { + return; + } + + late Color bgColor; + late Duration duration; + late String msg; + + try { + await KeyManager.changeSecurityKey(currentKey, newKey); + + msg = 'Successfully changed the security key!'; + bgColor = Colors.green; + duration = const Duration(seconds: 4); + } on Exception catch (e) { + msg = 'Failed to change security key! $e'; + bgColor = Colors.red; + duration = const Duration(seconds: 7); + } finally { + if (context.mounted) { + Navigator.pop(context); + } + if (outerContext.mounted) { + showSnackBar(outerContext, msg, bgColor, duration: duration); + } + } + } + + final inputFields = [ + ( + fieldKey: currentKeyStr, + fieldLabel: 'Current Security Key', + validateFunc: (key) => validateCurrentKey(key as String), + ), + ( + fieldKey: newKeyStr, + fieldLabel: 'New Security Key', + validateFunc: (key) => validateNewKey(key as String), + ), + ( + fieldKey: newKeyRepeatStr, + fieldLabel: 'Repeat New Security Key', + validateFunc: (key) => validateNewKeyRepeat(key as String), + ), + ]; + + // Use the unified SecurityKeyUI widget in dialog mode. + + final changeKeyForm = SecurityKeyUI( + webId: webId, + title: 'Change Security Key', + message: message, + inputFields: inputFields, + formKey: formKey, + submitFunc: submitForm, + displayMode: SecurityKeyDisplayMode.dialog, + child: child, + ); + + if (context.mounted) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + content: SingleChildScrollView( + child: changeKeyForm, + ), + contentPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + elevation: 0, + ), + ); + } + } +} diff --git a/lib/src/widgets/secret_text_field.dart b/lib/src/widgets/secret_text_field.dart new file mode 100644 index 0000000..33ad0c6 --- /dev/null +++ b/lib/src/widgets/secret_text_field.dart @@ -0,0 +1,120 @@ +/// A text field for inputting secret text (e.g. password, security key etc.) +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Dawei Chen + +library; + +import 'package:flutter/material.dart'; + +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +import 'package:solidui/src/constants/ui.dart'; + +/// A [StatefulWidget] for user to enter a secret text. + +class SecretTextField extends StatefulWidget { + /// Constructor. + + const SecretTextField({ + required this.fieldKey, + required this.fieldLabel, + required this.validateFunc, + super.key, + }); + + /// The key of this text field (to be used in a form). + + final String fieldKey; + + /// The label text. + + final String fieldLabel; + + /// The verification function. + + final String? Function(String) validateFunc; + + @override + State createState() => _SecretTextFieldState(); +} + +class _SecretTextFieldState extends State { + bool _showSecret = false; + + @override + Widget build(BuildContext context) { + // The label text style using theme-aware colour. + + final style = TextStyle( + color: SecurityThemeColors.labelGrey(context), + letterSpacing: 1.5, + fontSize: 13.0, + fontWeight: FontWeight.bold, + ); + + // The suffix icon with theme-aware colour. + + final iconColor = SecurityThemeColors.labelGrey(context); + final icon = IconButton( + icon: Icon( + _showSecret ? Icons.visibility : Icons.visibility_off, + color: iconColor, + ), + onPressed: () => setState(() { + // Toggle the state to show/hide the secret. + + _showSecret = !_showSecret; + }), + + // Does not participate in focus traversal (ignore TAB key). + + focusNode: FocusNode(skipTraversal: true), + ); + + // The validator. + + final secretValidator = FormBuilderValidators.compose([ + FormBuilderValidators.required(), + (val) => widget.validateFunc(val as String), + ]); + + return FormBuilderTextField( + name: widget.fieldKey, + obscureText: !_showSecret, + autocorrect: false, + decoration: InputDecoration( + labelText: widget.fieldLabel.toUpperCase(), + labelStyle: style, + suffixIcon: icon, + ), + style: TextStyle(color: SecurityThemeColors.text(context)), + cursorColor: SecurityThemeColors.primary(context), + validator: secretValidator, + ); + } +} diff --git a/lib/src/widgets/security_key_ui.dart b/lib/src/widgets/security_key_ui.dart new file mode 100644 index 0000000..dc664c7 --- /dev/null +++ b/lib/src/widgets/security_key_ui.dart @@ -0,0 +1,401 @@ +/// A unified widget for security key prompts and dialogs with WebID displayed prominently. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang + +library; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show LogicalKeyboardKey; + +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +import 'package:solidui/src/constants/ui.dart'; +import 'package:solidui/src/widgets/solid_login_helper.dart'; +import 'package:solidui/src/widgets/secret_text_field.dart'; + +/// Display mode for the SecurityKeyUI widget. +/// +/// This enum determines whether the widget should be displayed as a fullscreen prompt +/// or as an embedded dialog component. + +enum SecurityKeyDisplayMode { + /// Display as a fullscreen prompt with a scaffold. + + fullscreen, + + /// Display as an embedded dialog component. + + dialog +} + +/// A flexible [StatefulWidget] for security key operations with improved UI and WebID display. +/// +/// This widget can be used for both simple security key prompts (single input field) +/// and more complex dialogs (multiple input fields) by providing different configurations. + +class SecurityKeyUI extends StatefulWidget { + /// Constructor for the SecurityKeyUI widget. + /// + /// For a simple security key prompt: + /// - Pass a single input field in [inputFields] list + /// - Provide a title like "Security Key" + /// - Use [displayMode] = SecurityKeyDisplayMode.fullscreen + /// + /// For a security key dialog with multiple fields: + /// - Pass multiple input fields in [inputFields] + /// - Set [displayMode] = SecurityKeyDisplayMode.dialog + + const SecurityKeyUI({ + required this.webId, + required this.title, + required this.message, + required this.inputFields, + required this.formKey, + required this.submitFunc, + required this.child, + this.displayMode = SecurityKeyDisplayMode.fullscreen, + super.key, + }); + + /// The WebID to display. + + final String? webId; + + /// Title of the UI component. + + final String title; + + /// Message to display. + + final String message; + + /// The input text fields. + /// For a simple prompt, provide a list with a single field. + /// For a dialog with multiple inputs, provide multiple fields. + + final List< + ({ + String fieldKey, + String fieldLabel, + String? Function(String?) validateFunc, + })> inputFields; + + /// Key of the form for data retrieval. + + final GlobalKey formKey; + + /// The submit function. + + final Future Function(Map formDataMap) submitFunc; + + /// The child widget (for navigation after cancel). + + final Widget child; + + /// Display mode (fullscreen prompt or embedded dialog component). + + final SecurityKeyDisplayMode displayMode; + + @override + State createState() => _SecurityKeyUIState(); +} + +class _SecurityKeyUIState extends State { + Map _verifiedMap = {}; + bool _canSubmit = false; + + @override + void initState() { + super.initState(); + assert(widget.inputFields.isNotEmpty); + final fieldKeys = {for (final f in widget.inputFields) f.fieldKey}; + assert(fieldKeys.length == widget.inputFields.length); + _verifiedMap = {for (final k in fieldKeys) k: false}; + } + + Future _submit(BuildContext context) async { + final formData = + widget.formKey.currentState?.value as Map; + + if (!_canSubmit) { + return; + } + + for (final f in widget.inputFields) { + if (formData[f.fieldKey] == null) { + debugPrint('${f.fieldKey} is null'); + return; + } + } + + try { + await widget.submitFunc(formData); + } on Exception catch (e) { + debugPrint('$e'); + } + } + + @override + Widget build(BuildContext context) { + // Create the card content with the form. + + final cardContent = _buildCardContent(context); + + // Return based on display mode. + + if (widget.displayMode == SecurityKeyDisplayMode.fullscreen) { + return Scaffold( + backgroundColor: SecurityThemeColors.background(context), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: cardContent, + ), + ), + ); + } else { + return cardContent; + } + } + + /// Builds the card content including header, form fields, and buttons. + + Widget _buildCardContent(BuildContext context) { + return Container( + width: SecurityLayout.dialogWidth, + constraints: const BoxConstraints( + maxWidth: SecurityLayout.maxDialogWidth, + ), + decoration: BoxDecoration( + color: SecurityThemeColors.cardBackground(context), + borderRadius: BorderRadius.circular(SecurityLayout.borderRadius), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header section. + + _buildHeader(context), + + // Separator. + + Container( + height: SecurityLayout.separatorHeight, + color: SecurityThemeColors.separator(context), + ), + + // Form with input fields. + + Padding( + padding: SecurityLayout.formPadding, + child: _buildForm(), + ), + + // Buttons. + + Padding( + padding: SecurityLayout.buttonsPadding, + child: _buildButtons(context), + ), + ], + ), + ); + } + + /// Builds the header section with title, WebID, and message. + + Widget _buildHeader(BuildContext context) { + return Padding( + padding: SecurityLayout.contentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title heading. + + Text( + widget.title, + style: SecurityThemeTextStyles.heading(context), + ), + + // Green divider under heading. + + Container( + height: SecurityLayout.dividerHeight, + color: SecurityThemeColors.accent(context), + margin: SecurityLayout.dividerMargin, + ), + + // "Currently logged in as:" label. + + Text( + SecurityStrings.webIdLabel, + style: SecurityThemeTextStyles.label(context), + ), + + // WebID on separate line. + + Padding( + padding: SecurityLayout.webIdPadding, + child: Text( + widget.webId ?? SecurityStrings.notLoggedIn, + style: SecurityThemeTextStyles.webId( + context, + isLoggedIn: widget.webId != null, + ), + ), + ), + + // Instructions text. + + Text( + widget.message, + style: SecurityThemeTextStyles.body(context), + ), + ], + ), + ); + } + + /// Builds the form with input fields. + + Widget _buildForm() { + // Create the input fields. + + final inputFieldWidgets = []; + + for (final f in widget.inputFields) { + inputFieldWidgets.add( + Padding( + padding: SecurityLayout.inputFieldSpacing, + child: StatefulBuilder( + builder: (context, setState) => SecretTextField( + fieldKey: f.fieldKey, + fieldLabel: f.fieldLabel, + validateFunc: (val) { + final r = f.validateFunc(val); + + setState(() { + _verifiedMap[f.fieldKey] = (r == null); + }); + + this.setState(() { + _canSubmit = !_verifiedMap.containsValue(false); + }); + + return r; + }, + ), + ), + ), + ); + } + + return FormBuilder( + key: widget.formKey, + onChanged: () { + // Save input and validate. + + widget.formKey.currentState!.save(); + widget.formKey.currentState!.validate( + focusOnInvalid: false, + ); + + // Update state. + + setState(() { + _canSubmit = !_verifiedMap.containsValue(false); + }); + }, + autovalidateMode: AutovalidateMode.disabled, + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) async { + if (event.logicalKey == LogicalKeyboardKey.enter) { + await _submit(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: inputFieldWidgets, + ), + ), + ); + } + + /// Builds the buttons for submit and cancel. + + Widget _buildButtons(BuildContext context) { + final submitButton = ElevatedButton( + onPressed: _canSubmit ? () async => _submit(context) : null, + style: ElevatedButton.styleFrom( + backgroundColor: SecurityThemeColors.primary(context), + padding: SecurityLayout.buttonPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SecurityLayout.buttonRadius), + ), + ), + child: const Text( + SecurityStrings.submit, + style: SecurityThemeTextStyles.button, + ), + ); + + final cancelButton = TextButton( + onPressed: () { + if (widget.displayMode == SecurityKeyDisplayMode.dialog) { + Navigator.pop(context); + } else { + pushReplacement(context, widget.child); + } + }, + style: TextButton.styleFrom( + padding: SecurityLayout.buttonPadding, + ), + child: Text( + SecurityStrings.cancel, + style: SecurityThemeTextStyles.cancelButton(context), + ), + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + cancelButton, + SecurityLayout.horizontalGap, + submitButton, + ], + ); + } +} From b8672cd963b1117614fc7d8ca9f717fa62d5b61a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 30 Jan 2026 19:52:57 +1100 Subject: [PATCH 2/4] Lint --- example/pubspec.yaml | 2 +- lib/src/widgets/change_key_dialog.dart | 1 - lib/src/widgets/security_key_ui.dart | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 58bfe69..fac176e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter markdown_tooltip: 0.0.10 shared_preferences: ^2.5.4 - solidpod: ^0.9.8 + solidpod: ^0.9.12 solidui: path: .. window_manager: ^0.5.1 diff --git a/lib/src/widgets/change_key_dialog.dart b/lib/src/widgets/change_key_dialog.dart index 65855d0..0206b16 100644 --- a/lib/src/widgets/change_key_dialog.dart +++ b/lib/src/widgets/change_key_dialog.dart @@ -31,7 +31,6 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; - import 'package:solidpod/solidpod.dart' show NotLoggedInException, diff --git a/lib/src/widgets/security_key_ui.dart b/lib/src/widgets/security_key_ui.dart index dc664c7..917d156 100644 --- a/lib/src/widgets/security_key_ui.dart +++ b/lib/src/widgets/security_key_ui.dart @@ -34,8 +34,8 @@ import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:solidui/src/constants/ui.dart'; -import 'package:solidui/src/widgets/solid_login_helper.dart'; import 'package:solidui/src/widgets/secret_text_field.dart'; +import 'package:solidui/src/widgets/solid_login_helper.dart'; /// Display mode for the SecurityKeyUI widget. /// @@ -138,8 +138,7 @@ class _SecurityKeyUIState extends State { } Future _submit(BuildContext context) async { - final formData = - widget.formKey.currentState?.value as Map; + final formData = widget.formKey.currentState?.value as Map; if (!_canSubmit) { return; From def4ecf446d64a7dad7524170f5af09c34c3977c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 2 Feb 2026 22:23:31 +1100 Subject: [PATCH 3/4] Fix the issue that SecurityKeyUI, SecurityStrings, and changeKeyPopup are missing --- lib/src/constants/ui.dart | 159 ++++++++++++++++++ lib/src/handlers/solid_auth_handler.dart | 16 +- lib/src/utils/solid_pod_helpers.dart | 10 +- lib/src/widgets/solid_logout_dialog.dart | 36 +++- .../solid_security_key_manager_dialogs.dart | 3 +- 5 files changed, 204 insertions(+), 20 deletions(-) diff --git a/lib/src/constants/ui.dart b/lib/src/constants/ui.dart index 66013c2..6d3d9b0 100644 --- a/lib/src/constants/ui.dart +++ b/lib/src/constants/ui.dart @@ -503,6 +503,10 @@ class GrantPermFormLayout { static const contentPadding = EdgeInsets.symmetric(horizontal: 50); + /// Padding for dialog input sections. + + static const inputPadding = EdgeInsets.all(8); + /// Standard width for security dialogs. static const dialogWidth = 480.0; @@ -519,3 +523,158 @@ class GrantPermFormLayout { static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); } + +/// Small vertical spacing for widgets. + +const smallGapV = SizedBox(height: 10.0); + +/// Large vertical spacing for widgets. + +const largeGapV = SizedBox(height: 40.0); + +/// Text styles used for permission form. + +class RecipientTextStyle { + /// Style for the label. + + static const label = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ); + + /// Style for the WebID display. + + static const webId = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.blueAccent, + ); +} + +/// Layout constants for sub headings. + +class SubHeadingStyle { + /// Font size. + + static const double fontsize = 17.0; + + /// Font colour. + + static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); + + /// Font weight. + + static const FontWeight fontweight = FontWeight.bold; + + /// Padding. + + static const double padding = 8.0; +} + +/// Layout constants for headings. + +class HeadingStyle { + /// Font size. + + static const double fontsize = 22.0; + + /// Font colour. + + static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); + + /// Font weight. + + static const FontWeight fontweight = FontWeight.bold; + + /// Padding. + + static const double padding = 8.0; +} + +/// Build a heading widget with customisable style. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [fontSize] - The font size. +/// - [fontWeight] - The font weight (optional). +/// - [color] - The text colour (optional). +/// - [padding] - The padding around the text (optional). + +Row buildHeading({ + required String text, + required double fontSize, + FontWeight? fontWeight, + Color? color, + double? padding, +}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Padding( + padding: padding == null ? EdgeInsets.zero : EdgeInsets.all(padding), + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight ?? FontWeight.normal, + color: color ?? Colors.black, + ), + ), + ), + ), + ], + ); +} + +/// Make a sub heading using SubHeadingStyle as default. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [bold] - Whether to use bold font weight. +/// - [addColor] - Whether to add the default colour. +/// - [addPadding] - Whether to add padding. + +Widget makeSubHeading( + String text, { + bool bold = true, + bool addColor = true, + bool addPadding = true, +}) => + buildHeading( + text: text, + fontSize: SubHeadingStyle.fontsize, + fontWeight: (bold) ? SubHeadingStyle.fontweight : FontWeight.normal, + color: (addColor) ? SubHeadingStyle.fontcolor : Colors.black, + padding: (addPadding) ? SubHeadingStyle.padding : 0, + ); + +/// Make a heading using HeadingStyle as default. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [bold] - Whether to use bold font weight. +/// - [addColor] - Whether to add the default colour. +/// - [addPadding] - Whether to add padding. + +Widget makeHeading( + String text, { + bool bold = true, + bool addColor = true, + bool addPadding = true, +}) => + buildHeading( + text: text, + fontSize: HeadingStyle.fontsize, + fontWeight: (bold) ? HeadingStyle.fontweight : FontWeight.normal, + color: (addColor) ? HeadingStyle.fontcolor : Colors.black, + padding: (addPadding) ? HeadingStyle.padding : 0, + ); + +/// Layout constants used for sharing page. + +class SharingPageLayout { + /// Padding for dialog input sections. + + static const inputPadding = EdgeInsets.all(8); +} diff --git a/lib/src/handlers/solid_auth_handler.dart b/lib/src/handlers/solid_auth_handler.dart index 72e7ba8..cf3a743 100644 --- a/lib/src/handlers/solid_auth_handler.dart +++ b/lib/src/handlers/solid_auth_handler.dart @@ -154,16 +154,20 @@ class SolidAuthHandler { /// Handle logout functionality with confirmation popup. Future handleLogout(BuildContext context) async { - if (_config?.onSecurityKeyReset != null) { - _config!.onSecurityKeyReset!(); - } - // Use login page as the return destination to avoid going back to main app. final returnWidget = _buildLoginPage(context); - await logoutPopup(context, returnWidget); - // After logout popup, the user should already be on the login page + // Pass the onSecurityKeyReset callback to logoutPopup so it is invoked + // only AFTER logoutPod succeeds. + + await logoutPopup( + context, + returnWidget, + onLogoutSuccess: _config?.onSecurityKeyReset, + ); + + // After logout popup, the user should already be on the login page. // No additional navigation needed. } diff --git a/lib/src/utils/solid_pod_helpers.dart b/lib/src/utils/solid_pod_helpers.dart index 9cd27e0..cbcebbb 100644 --- a/lib/src/utils/solid_pod_helpers.dart +++ b/lib/src/utils/solid_pod_helpers.dart @@ -32,14 +32,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:solidpod/solidpod.dart' - show - isUserLoggedIn, - getWebId, - KeyManager, - verifySecurityKey, - SecurityKeyUI, - SecurityStrings; + show isUserLoggedIn, getWebId, KeyManager, verifySecurityKey; +import 'package:solidui/src/constants/ui.dart' show SecurityStrings; +import 'package:solidui/src/widgets/security_key_ui.dart' show SecurityKeyUI; import 'package:solidui/src/widgets/solid_login_webid_input_dialog.dart'; /// Login if the user has not done so. diff --git a/lib/src/widgets/solid_logout_dialog.dart b/lib/src/widgets/solid_logout_dialog.dart index 1be7e7c..a235970 100644 --- a/lib/src/widgets/solid_logout_dialog.dart +++ b/lib/src/widgets/solid_logout_dialog.dart @@ -1,4 +1,4 @@ -/// Copyright (C) 2024, Software Innovation Institute, ANU. +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. /// /// Licensed under the MIT License (the "License"). /// @@ -22,7 +22,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. /// -/// Authors: Dawei Chen +/// Authors: Dawei Chen, Tony Chen library; @@ -35,12 +35,23 @@ import 'package:solidpod/solidpod.dart' show getAppNameVersion, logoutPod; class LogoutDialog extends StatefulWidget { /// Constructor. - const LogoutDialog({required this.child, super.key}); + const LogoutDialog({ + required this.child, + this.onLogoutSuccess, + super.key, + }); /// The child widget after logging out. final Widget child; + /// Optional callback invoked after successful logout. + /// This is called AFTER [logoutPod] completes successfully but BEFORE + /// navigation occurs. Use this to reset UI state such as security key + /// status notifiers. + + final VoidCallback? onLogoutSuccess; + @override State createState() => _LogoutDialogState(); } @@ -55,6 +66,8 @@ class _LogoutDialogState extends State { child: const Text('OK'), onPressed: () async { if (await logoutPod()) { + widget.onLogoutSuccess?.call(); + if (context.mounted) { await Navigator.pushReplacement( context, @@ -115,10 +128,23 @@ class _LogoutDialogState extends State { } /// Display a pop up dialog for logging out. +/// +/// Parameters: +/// - [context] - The build context. +/// - [child] - The widget to navigate to after successful logout. +/// - [onLogoutSuccess] - Optional callback invoked after successful logout +/// but before navigation. Use this to reset UI state managers. -Future logoutPopup(BuildContext context, Widget child) async { +Future logoutPopup( + BuildContext context, + Widget child, { + VoidCallback? onLogoutSuccess, +}) async { await showDialog( context: context, - builder: (context) => LogoutDialog(child: child), + builder: (context) => LogoutDialog( + child: child, + onLogoutSuccess: onLogoutSuccess, + ), ); } diff --git a/lib/src/widgets/solid_security_key_manager_dialogs.dart b/lib/src/widgets/solid_security_key_manager_dialogs.dart index 0da2a1c..6126bd6 100644 --- a/lib/src/widgets/solid_security_key_manager_dialogs.dart +++ b/lib/src/widgets/solid_security_key_manager_dialogs.dart @@ -30,8 +30,7 @@ library; import 'package:flutter/material.dart'; -import 'package:solidpod/solidpod.dart' show changeKeyPopup; - +import 'package:solidui/src/widgets/change_key_dialog.dart' show changeKeyPopup; import 'package:solidui/src/widgets/solid_security_key_cache_dialogs.dart'; import 'package:solidui/src/widgets/solid_security_key_set_dialog.dart'; import 'package:solidui/src/widgets/solid_security_key_ui_helpers.dart'; From a3c8a0fbe708d1e5f44c6423fbd67f3e173bb55c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 2 Feb 2026 22:33:39 +1100 Subject: [PATCH 4/4] Lint (locmax) --- lib/src/constants/ui.dart | 651 +---------------------- lib/src/constants/ui_colors.dart | 181 +++++++ lib/src/constants/ui_common.dart | 150 ++++++ lib/src/constants/ui_layout.dart | 178 +++++++ lib/src/constants/ui_strings.dart | 54 ++ lib/src/constants/ui_text_styles.dart | 178 +++++++ lib/src/constants/ui_window.dart | 99 ++++ lib/src/widgets/solid_logout_dialog.dart | 2 +- 8 files changed, 853 insertions(+), 640 deletions(-) create mode 100644 lib/src/constants/ui_colors.dart create mode 100644 lib/src/constants/ui_common.dart create mode 100644 lib/src/constants/ui_layout.dart create mode 100644 lib/src/constants/ui_strings.dart create mode 100644 lib/src/constants/ui_text_styles.dart create mode 100644 lib/src/constants/ui_window.dart diff --git a/lib/src/constants/ui.dart b/lib/src/constants/ui.dart index 6d3d9b0..de7d9ff 100644 --- a/lib/src/constants/ui.dart +++ b/lib/src/constants/ui.dart @@ -28,653 +28,26 @@ library; -import 'package:flutter/material.dart'; +// Window and list item size constants. -/// Thresholds for window size. +export 'ui_window.dart'; -class WindowSize { - /// Small width threshold. +// Colour constants. - static const double smallWidthLimit = 600; +export 'ui_colors.dart'; - /// Small height threshold. +// Text style constants. - static const double smallHeightLimit = 600; +export 'ui_text_styles.dart'; - /// Boolean describing whether the parent widget is narrow. - /// Derived from the box constraints found by LayoutBuilder(). - /// - /// Arguments: - /// - [constraints] - The box constraints of the parent widget - /// where LayoutBuilder() was called. +// Layout constants. - bool isNarrowWindow(BoxConstraints constraints) { - final bool isNarrow; - if (constraints.maxWidth < WindowSize.smallWidthLimit) { - isNarrow = true; - } else { - isNarrow = false; - } +export 'ui_layout.dart'; - return isNarrow; - } -} +// String constants. -/// Approximate size for grid items used for displaying text in list item. +export 'ui_strings.dart'; -class ListItemSize { - /// Approximate height of compressed item in list when list item text - /// is line wrapped in a narrow mobile phone size window. - /// (Where each of note title, created date time, modified date time - /// are line wrapped to two lines.) +// Common constants and helper functions. - static const double compressedItemHeight = 260; - - /// Approximate height of uncompressed item in list when list item text - /// is not line wrapped. - - static const double uncompressedItemHeight = 138; - - /// Calculate card aspect ratio to use for gridview builder cards - /// using the box constraints found by LayoutBuilder(). - /// - /// Arguments: - /// - [constraints] - The box constraints of the parent widget - /// where LayoutBuilder() was called. - - double calculateCardAspectRatio(BoxConstraints constraints) { - /// Aspect ratio (width / height) for gridview cards to display note items. - final double cardAspectRatio; - - // Derive card aspect ratio (width / height). - if (constraints.maxWidth < WindowSize.smallWidthLimit) { - cardAspectRatio = constraints.maxWidth / compressedItemHeight; - } else { - cardAspectRatio = constraints.maxWidth / uncompressedItemHeight; - } - return cardAspectRatio; - } -} - -/// Class for icon sizing in list items. - -class ListIconSize { - /// Icon width. - - static const double width = 50; - - /// Icon height. - - static const double height = 50; - - /// Width for two icons side by side. - - static const double twoIconWidth = (width * 2) + gap; - - /// Gap between icons. - - static const double gap = 15; -} - -/// Icon shape decoration for list items. - -ShapeDecoration listIconShape = - const ShapeDecoration(color: Colors.grey, shape: CircleBorder()); - -// Standard colours for actions and results. - -class ActionColors { - /// Green colour used for success. - - static const success = Colors.green; - - // Red colour used for error/failure. - - static const error = Colors.red; - - // Colour used for warning. - - static const warning = Color.fromARGB(255, 204, 99, 1); - - // Red colour used for delete action. - - static const delete = Colors.red; -} - -/// Colours used across security dialogs and prompts. - -class SecurityColors { - /// Primary colour (Forest Green) used for headings and important elements. - - static const primary = Color(0xFF2E7D32); - - /// Primary colour for dark mode (lighter green for better contrast). - - static const primaryDark = Color(0xFF66BB6A); - - /// Accent colour (Lighter Green) used for dividers and secondary elements. - - static const accent = Color(0xFF4CAF50); - - /// Accent colour for dark mode. - - static const accentDark = Color(0xFF81C784); - - /// Background colour (Light Grey) used for dialog backgrounds. - - static const background = Color(0xFFF5F5F5); - - /// Background colour for dark mode. - - static const backgroundDark = Color(0xFF1E1E1E); - - /// Text colour (Dark Grey) used for main text content. - - static const text = Color(0xFF212121); - - /// Text colour for dark mode. - - static const textDark = Color(0xFFE0E0E0); - - /// Grey colour used for labels and secondary text. - - static const labelGrey = Colors.grey; - - /// Label colour for dark mode. - - static const labelGreyDark = Color(0xFF9E9E9E); - - /// Card background colour for light mode. - - static const cardBackground = Colors.white; - - /// Card background colour for dark mode. - - static const cardBackgroundDark = Color(0xFF2D2D2D); - - /// Separator colour for light mode. - - static const separator = Color(0xFFE0E0E0); - - /// Separator colour for dark mode. - - static const separatorDark = Color(0xFF424242); -} - -/// Helper class to obtain theme-aware colours for security UI components. -/// -/// This class provides methods that return appropriate colours based on the -/// current theme brightness (light or dark mode). - -class SecurityThemeColors { - /// Returns the primary colour based on the current theme. - - static Color primary(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.primaryDark : SecurityColors.primary; - } - - /// Returns the accent colour based on the current theme. - - static Color accent(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.accentDark : SecurityColors.accent; - } - - /// Returns the background colour based on the current theme. - - static Color background(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.backgroundDark : SecurityColors.background; - } - - /// Returns the text colour based on the current theme. - - static Color text(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.textDark : SecurityColors.text; - } - - /// Returns the label grey colour based on the current theme. - - static Color labelGrey(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.labelGreyDark : SecurityColors.labelGrey; - } - - /// Returns the card background colour based on the current theme. - - static Color cardBackground(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark - ? SecurityColors.cardBackgroundDark - : SecurityColors.cardBackground; - } - - /// Returns the separator colour based on the current theme. - - static Color separator(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? SecurityColors.separatorDark : SecurityColors.separator; - } -} - -/// Text styles used across security dialogs and prompts. - -class SecurityTextStyles { - /// Style for main headings (e.g. "Security Key"). - - static const heading = TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: SecurityColors.primary, - ); - - /// Style for regular text content. - - static const body = TextStyle(fontSize: 15, color: SecurityColors.text); - - /// Style for the WebID display. - - static const webId = TextStyle(fontSize: 13, fontWeight: FontWeight.w500); - - /// Style for the "Currently logged in as:" label. - - static const label = TextStyle(fontSize: 13, color: SecurityColors.labelGrey); - - /// Style for button text. - - static const button = TextStyle(fontSize: 14, color: Colors.white); -} - -/// Helper class to obtain theme-aware text styles for security UI components. - -class SecurityThemeTextStyles { - /// Returns the heading style based on the current theme. - - static TextStyle heading(BuildContext context) { - return TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: SecurityThemeColors.primary(context), - ); - } - - /// Returns the body text style based on the current theme. - - static TextStyle body(BuildContext context) { - return TextStyle( - fontSize: 15, - color: SecurityThemeColors.text(context), - ); - } - - /// Returns the WebID style based on the current theme. - - static TextStyle webId(BuildContext context, {bool isLoggedIn = true}) { - return TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: isLoggedIn ? SecurityThemeColors.primary(context) : Colors.red, - ); - } - - /// Returns the label style based on the current theme. - - static TextStyle label(BuildContext context) { - return TextStyle( - fontSize: 13, - color: SecurityThemeColors.labelGrey(context), - ); - } - - /// Returns the button text style. - - static const button = TextStyle( - fontSize: 14, - color: Colors.white, - ); - - /// Returns the cancel button style based on the current theme. - - static TextStyle cancelButton(BuildContext context) { - return TextStyle( - fontSize: 14, - color: SecurityThemeColors.text(context), - ); - } -} - -/// Layout constants used across security dialogs and prompts. - -class SecurityLayout { - /// Horizontal gap between elements. - - static const horizontalGap = SizedBox(width: 16); - - /// Standard padding for dialog content. - - static const contentPadding = EdgeInsets.all(20); - - /// Padding for form sections. - - static const formPadding = EdgeInsets.fromLTRB(20, 20, 20, 8); - - /// Padding for button sections. - - static const buttonsPadding = EdgeInsets.fromLTRB(20, 8, 20, 20); - - /// Margin for green divider under heading. - - static const dividerMargin = EdgeInsets.only(top: 4, bottom: 14); - - /// Padding for WebID display. - - static const webIdPadding = EdgeInsets.only(top: 4, bottom: 20); - - /// Input field spacing. - - static const inputFieldSpacing = EdgeInsets.only(bottom: 16); - - /// Button padding. - - static const buttonPadding = EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ); - - /// Standard width for security dialogs. - - static const dialogWidth = 480.0; - - /// Maximum width constraint for security dialogs. - - static const maxDialogWidth = 500.0; - - /// Border radius for cards and buttons. - - static const borderRadius = 8.0; - - /// Border radius for buttons. - - static const buttonRadius = 6.0; - - /// Height for divider lines. - - static const dividerHeight = 1.5; - - /// Height for separator lines. - - static const separatorHeight = 1.0; -} - -/// Common text strings used across security dialogs and prompts. - -class SecurityStrings { - /// Label for the WebID display. - - static const webIdLabel = 'Currently logged in as:'; - - /// Label for not logged in state. - - static const notLoggedIn = 'Not logged in'; - - /// Security key input prompt. - - static const securityKeyPrompt = - 'Please enter the security key you previously provided for securing your data.'; - - /// Submit button text. - - static const submit = 'Submit'; - - /// Cancel button text. - - static const cancel = 'Cancel'; -} - -/// Layout constants for scrollbars. - -class ScrollbarLayout { - /// Vertical gap between edge widget and scrollbar to avoid - /// horizontal scrollbar overlapping bottom edge of wrapped - /// content. - - static const verticalGap = SizedBox(height: 30); - - /// Horizontal gap between edge widget and scrollbar to avoid - /// vertical scrollbar overlapping the right edge of wrapped - /// content - /// - static const horizontalGap = SizedBox(width: 10); -} - -/// Normal height for data loading screens -const double normalLoadingScreenHeight = 200.0; - -/// Colours used across dropdown dialogs and prompts. - -class DropdownColors { - /// Primary colour (Forest Green) used for dropdown elements - - static const primary = Color(0xFF2E7D32); - - /// Accent colour (Lighter Green) used for dividers and secondary elements. - - static const accent = Color(0xFF4CAF50); -} - -/// Layout constants used for WebId dialogs. - -class WebIdLayout { - /// Vertical gap between paragraphs. - - static const paraVertGap = SizedBox(height: 10); - - /// Standard padding for dialog content. - - static const contentPadding = EdgeInsets.symmetric(horizontal: 50); - - /// Standard width for security dialogs. - - static const dialogWidth = 480.0; - - /// Height of dropdown suggestion box. - - static const dropdownHeight = 120.0; - - /// Elevation of dropdown suggestion cards. - - static double dropdownElevation = 5; - - /// Padding of dropdown suggestion list. - - static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); -} - -/// Layout constants used for Grant Permission Form Dialog. - -class GrantPermFormLayout { - /// Vertical gap between paragraphs. - - static const paraVertGap = SizedBox(height: 10); - - /// Standard padding for dialog content. - - static const contentPadding = EdgeInsets.symmetric(horizontal: 50); - - /// Padding for dialog input sections. - - static const inputPadding = EdgeInsets.all(8); - - /// Standard width for security dialogs. - - static const dialogWidth = 480.0; - - /// Height of dropdown suggestion box. - - static const dropdownHeight = 120.0; - - /// Elevation of dropdown suggestion cards. - - static double dropdownElevation = 5; - - /// Padding of dropdown suggestion list. - - static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); -} - -/// Small vertical spacing for widgets. - -const smallGapV = SizedBox(height: 10.0); - -/// Large vertical spacing for widgets. - -const largeGapV = SizedBox(height: 40.0); - -/// Text styles used for permission form. - -class RecipientTextStyle { - /// Style for the label. - - static const label = TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - ); - - /// Style for the WebID display. - - static const webId = TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.blueAccent, - ); -} - -/// Layout constants for sub headings. - -class SubHeadingStyle { - /// Font size. - - static const double fontsize = 17.0; - - /// Font colour. - - static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); - - /// Font weight. - - static const FontWeight fontweight = FontWeight.bold; - - /// Padding. - - static const double padding = 8.0; -} - -/// Layout constants for headings. - -class HeadingStyle { - /// Font size. - - static const double fontsize = 22.0; - - /// Font colour. - - static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); - - /// Font weight. - - static const FontWeight fontweight = FontWeight.bold; - - /// Padding. - - static const double padding = 8.0; -} - -/// Build a heading widget with customisable style. -/// -/// Arguments: -/// - [text] - The text to display. -/// - [fontSize] - The font size. -/// - [fontWeight] - The font weight (optional). -/// - [color] - The text colour (optional). -/// - [padding] - The padding around the text (optional). - -Row buildHeading({ - required String text, - required double fontSize, - FontWeight? fontWeight, - Color? color, - double? padding, -}) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Padding( - padding: padding == null ? EdgeInsets.zero : EdgeInsets.all(padding), - child: Text( - text, - style: TextStyle( - fontSize: fontSize, - fontWeight: fontWeight ?? FontWeight.normal, - color: color ?? Colors.black, - ), - ), - ), - ), - ], - ); -} - -/// Make a sub heading using SubHeadingStyle as default. -/// -/// Arguments: -/// - [text] - The text to display. -/// - [bold] - Whether to use bold font weight. -/// - [addColor] - Whether to add the default colour. -/// - [addPadding] - Whether to add padding. - -Widget makeSubHeading( - String text, { - bool bold = true, - bool addColor = true, - bool addPadding = true, -}) => - buildHeading( - text: text, - fontSize: SubHeadingStyle.fontsize, - fontWeight: (bold) ? SubHeadingStyle.fontweight : FontWeight.normal, - color: (addColor) ? SubHeadingStyle.fontcolor : Colors.black, - padding: (addPadding) ? SubHeadingStyle.padding : 0, - ); - -/// Make a heading using HeadingStyle as default. -/// -/// Arguments: -/// - [text] - The text to display. -/// - [bold] - Whether to use bold font weight. -/// - [addColor] - Whether to add the default colour. -/// - [addPadding] - Whether to add padding. - -Widget makeHeading( - String text, { - bool bold = true, - bool addColor = true, - bool addPadding = true, -}) => - buildHeading( - text: text, - fontSize: HeadingStyle.fontsize, - fontWeight: (bold) ? HeadingStyle.fontweight : FontWeight.normal, - color: (addColor) ? HeadingStyle.fontcolor : Colors.black, - padding: (addPadding) ? HeadingStyle.padding : 0, - ); - -/// Layout constants used for sharing page. - -class SharingPageLayout { - /// Padding for dialog input sections. - - static const inputPadding = EdgeInsets.all(8); -} +export 'ui_common.dart'; diff --git a/lib/src/constants/ui_colors.dart b/lib/src/constants/ui_colors.dart new file mode 100644 index 0000000..efe3d18 --- /dev/null +++ b/lib/src/constants/ui_colors.dart @@ -0,0 +1,181 @@ +/// Colour constants for UI elements. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +import 'package:flutter/material.dart'; + +// Standard colours for actions and results. + +class ActionColors { + /// Green colour used for success. + + static const success = Colors.green; + + // Red colour used for error/failure. + + static const error = Colors.red; + + // Colour used for warning. + + static const warning = Color.fromARGB(255, 204, 99, 1); + + // Red colour used for delete action. + + static const delete = Colors.red; +} + +/// Colours used across security dialogs and prompts. + +class SecurityColors { + /// Primary colour (Forest Green) used for headings and important elements. + + static const primary = Color(0xFF2E7D32); + + /// Primary colour for dark mode (lighter green for better contrast). + + static const primaryDark = Color(0xFF66BB6A); + + /// Accent colour (Lighter Green) used for dividers and secondary elements. + + static const accent = Color(0xFF4CAF50); + + /// Accent colour for dark mode. + + static const accentDark = Color(0xFF81C784); + + /// Background colour (Light Grey) used for dialog backgrounds. + + static const background = Color(0xFFF5F5F5); + + /// Background colour for dark mode. + + static const backgroundDark = Color(0xFF1E1E1E); + + /// Text colour (Dark Grey) used for main text content. + + static const text = Color(0xFF212121); + + /// Text colour for dark mode. + + static const textDark = Color(0xFFE0E0E0); + + /// Grey colour used for labels and secondary text. + + static const labelGrey = Colors.grey; + + /// Label colour for dark mode. + + static const labelGreyDark = Color(0xFF9E9E9E); + + /// Card background colour for light mode. + + static const cardBackground = Colors.white; + + /// Card background colour for dark mode. + + static const cardBackgroundDark = Color(0xFF2D2D2D); + + /// Separator colour for light mode. + + static const separator = Color(0xFFE0E0E0); + + /// Separator colour for dark mode. + + static const separatorDark = Color(0xFF424242); +} + +/// Helper class to obtain theme-aware colours for security UI components. +/// +/// This class provides methods that return appropriate colours based on the +/// current theme brightness (light or dark mode). + +class SecurityThemeColors { + /// Returns the primary colour based on the current theme. + + static Color primary(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.primaryDark : SecurityColors.primary; + } + + /// Returns the accent colour based on the current theme. + + static Color accent(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.accentDark : SecurityColors.accent; + } + + /// Returns the background colour based on the current theme. + + static Color background(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.backgroundDark : SecurityColors.background; + } + + /// Returns the text colour based on the current theme. + + static Color text(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.textDark : SecurityColors.text; + } + + /// Returns the label grey colour based on the current theme. + + static Color labelGrey(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.labelGreyDark : SecurityColors.labelGrey; + } + + /// Returns the card background colour based on the current theme. + + static Color cardBackground(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark + ? SecurityColors.cardBackgroundDark + : SecurityColors.cardBackground; + } + + /// Returns the separator colour based on the current theme. + + static Color separator(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return isDark ? SecurityColors.separatorDark : SecurityColors.separator; + } +} + +/// Colours used across dropdown dialogs and prompts. + +class DropdownColors { + /// Primary colour (Forest Green) used for dropdown elements + + static const primary = Color(0xFF2E7D32); + + /// Accent colour (Lighter Green) used for dividers and secondary elements. + + static const accent = Color(0xFF4CAF50); +} diff --git a/lib/src/constants/ui_common.dart b/lib/src/constants/ui_common.dart new file mode 100644 index 0000000..ed61c5c --- /dev/null +++ b/lib/src/constants/ui_common.dart @@ -0,0 +1,150 @@ +/// Common UI constants and helper functions. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/src/constants/ui_text_styles.dart'; + +/// Class for icon sizing in list items. + +class ListIconSize { + /// Icon width. + + static const double width = 50; + + /// Icon height. + + static const double height = 50; + + /// Width for two icons side by side. + + static const double twoIconWidth = (width * 2) + gap; + + /// Gap between icons. + + static const double gap = 15; +} + +/// Icon shape decoration for list items. + +ShapeDecoration listIconShape = + const ShapeDecoration(color: Colors.grey, shape: CircleBorder()); + +/// Normal height for data loading screens. + +const double normalLoadingScreenHeight = 200.0; + +/// Small vertical spacing for widgets. + +const smallGapV = SizedBox(height: 10.0); + +/// Large vertical spacing for widgets. + +const largeGapV = SizedBox(height: 40.0); + +/// Build a heading widget with customisable style. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [fontSize] - The font size. +/// - [fontWeight] - The font weight (optional). +/// - [color] - The text colour (optional). +/// - [padding] - The padding around the text (optional). + +Row buildHeading({ + required String text, + required double fontSize, + FontWeight? fontWeight, + Color? color, + double? padding, +}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Padding( + padding: padding == null ? EdgeInsets.zero : EdgeInsets.all(padding), + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight ?? FontWeight.normal, + color: color ?? Colors.black, + ), + ), + ), + ), + ], + ); +} + +/// Make a sub heading using SubHeadingStyle as default. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [bold] - Whether to use bold font weight. +/// - [addColor] - Whether to add the default colour. +/// - [addPadding] - Whether to add padding. + +Widget makeSubHeading( + String text, { + bool bold = true, + bool addColor = true, + bool addPadding = true, +}) => + buildHeading( + text: text, + fontSize: SubHeadingStyle.fontsize, + fontWeight: (bold) ? SubHeadingStyle.fontweight : FontWeight.normal, + color: (addColor) ? SubHeadingStyle.fontcolor : Colors.black, + padding: (addPadding) ? SubHeadingStyle.padding : 0, + ); + +/// Make a heading using HeadingStyle as default. +/// +/// Arguments: +/// - [text] - The text to display. +/// - [bold] - Whether to use bold font weight. +/// - [addColor] - Whether to add the default colour. +/// - [addPadding] - Whether to add padding. + +Widget makeHeading( + String text, { + bool bold = true, + bool addColor = true, + bool addPadding = true, +}) => + buildHeading( + text: text, + fontSize: HeadingStyle.fontsize, + fontWeight: (bold) ? HeadingStyle.fontweight : FontWeight.normal, + color: (addColor) ? HeadingStyle.fontcolor : Colors.black, + padding: (addPadding) ? HeadingStyle.padding : 0, + ); diff --git a/lib/src/constants/ui_layout.dart b/lib/src/constants/ui_layout.dart new file mode 100644 index 0000000..8ce9f24 --- /dev/null +++ b/lib/src/constants/ui_layout.dart @@ -0,0 +1,178 @@ +/// Layout constants for UI elements. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +import 'package:flutter/material.dart'; + +/// Layout constants used across security dialogs and prompts. + +class SecurityLayout { + /// Horizontal gap between elements. + + static const horizontalGap = SizedBox(width: 16); + + /// Standard padding for dialog content. + + static const contentPadding = EdgeInsets.all(20); + + /// Padding for form sections. + + static const formPadding = EdgeInsets.fromLTRB(20, 20, 20, 8); + + /// Padding for button sections. + + static const buttonsPadding = EdgeInsets.fromLTRB(20, 8, 20, 20); + + /// Margin for green divider under heading. + + static const dividerMargin = EdgeInsets.only(top: 4, bottom: 14); + + /// Padding for WebID display. + + static const webIdPadding = EdgeInsets.only(top: 4, bottom: 20); + + /// Input field spacing. + + static const inputFieldSpacing = EdgeInsets.only(bottom: 16); + + /// Button padding. + + static const buttonPadding = EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ); + + /// Standard width for security dialogs. + + static const dialogWidth = 480.0; + + /// Maximum width constraint for security dialogs. + + static const maxDialogWidth = 500.0; + + /// Border radius for cards and buttons. + + static const borderRadius = 8.0; + + /// Border radius for buttons. + + static const buttonRadius = 6.0; + + /// Height for divider lines. + + static const dividerHeight = 1.5; + + /// Height for separator lines. + + static const separatorHeight = 1.0; +} + +/// Layout constants for scrollbars. + +class ScrollbarLayout { + /// Vertical gap between edge widget and scrollbar to avoid + /// horizontal scrollbar overlapping bottom edge of wrapped + /// content. + + static const verticalGap = SizedBox(height: 30); + + /// Horizontal gap between edge widget and scrollbar to avoid + /// vertical scrollbar overlapping the right edge of wrapped + /// content + + static const horizontalGap = SizedBox(width: 10); +} + +/// Layout constants used for WebId dialogs. + +class WebIdLayout { + /// Vertical gap between paragraphs. + + static const paraVertGap = SizedBox(height: 10); + + /// Standard padding for dialog content. + + static const contentPadding = EdgeInsets.symmetric(horizontal: 50); + + /// Standard width for security dialogs. + + static const dialogWidth = 480.0; + + /// Height of dropdown suggestion box. + + static const dropdownHeight = 120.0; + + /// Elevation of dropdown suggestion cards. + + static double dropdownElevation = 5; + + /// Padding of dropdown suggestion list. + + static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); +} + +/// Layout constants used for Grant Permission Form Dialog. + +class GrantPermFormLayout { + /// Vertical gap between paragraphs. + + static const paraVertGap = SizedBox(height: 10); + + /// Standard padding for dialog content. + + static const contentPadding = EdgeInsets.symmetric(horizontal: 50); + + /// Padding for dialog input sections. + + static const inputPadding = EdgeInsets.all(8); + + /// Standard width for security dialogs. + + static const dialogWidth = 480.0; + + /// Height of dropdown suggestion box. + + static const dropdownHeight = 120.0; + + /// Elevation of dropdown suggestion cards. + + static double dropdownElevation = 5; + + /// Padding of dropdown suggestion list. + + static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); +} + +/// Layout constants used for sharing page. + +class SharingPageLayout { + /// Padding for dialog input sections. + + static const inputPadding = EdgeInsets.all(8); +} diff --git a/lib/src/constants/ui_strings.dart b/lib/src/constants/ui_strings.dart new file mode 100644 index 0000000..6a13202 --- /dev/null +++ b/lib/src/constants/ui_strings.dart @@ -0,0 +1,54 @@ +/// String constants for UI elements. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +/// Common text strings used across security dialogs and prompts. + +class SecurityStrings { + /// Label for the WebID display. + + static const webIdLabel = 'Currently logged in as:'; + + /// Label for not logged in state. + + static const notLoggedIn = 'Not logged in'; + + /// Security key input prompt. + + static const securityKeyPrompt = + 'Please enter the security key you previously provided for securing your data.'; + + /// Submit button text. + + static const submit = 'Submit'; + + /// Cancel button text. + + static const cancel = 'Cancel'; +} diff --git a/lib/src/constants/ui_text_styles.dart b/lib/src/constants/ui_text_styles.dart new file mode 100644 index 0000000..8145f32 --- /dev/null +++ b/lib/src/constants/ui_text_styles.dart @@ -0,0 +1,178 @@ +/// Text style constants for UI elements. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/src/constants/ui_colors.dart'; + +/// Text styles used across security dialogs and prompts. + +class SecurityTextStyles { + /// Style for main headings (e.g. "Security Key"). + + static const heading = TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: SecurityColors.primary, + ); + + /// Style for regular text content. + + static const body = TextStyle(fontSize: 15, color: SecurityColors.text); + + /// Style for the WebID display. + + static const webId = TextStyle(fontSize: 13, fontWeight: FontWeight.w500); + + /// Style for the "Currently logged in as:" label. + + static const label = TextStyle(fontSize: 13, color: SecurityColors.labelGrey); + + /// Style for button text. + + static const button = TextStyle(fontSize: 14, color: Colors.white); +} + +/// Helper class to obtain theme-aware text styles for security UI components. + +class SecurityThemeTextStyles { + /// Returns the heading style based on the current theme. + + static TextStyle heading(BuildContext context) { + return TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: SecurityThemeColors.primary(context), + ); + } + + /// Returns the body text style based on the current theme. + + static TextStyle body(BuildContext context) { + return TextStyle( + fontSize: 15, + color: SecurityThemeColors.text(context), + ); + } + + /// Returns the WebID style based on the current theme. + + static TextStyle webId(BuildContext context, {bool isLoggedIn = true}) { + return TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isLoggedIn ? SecurityThemeColors.primary(context) : Colors.red, + ); + } + + /// Returns the label style based on the current theme. + + static TextStyle label(BuildContext context) { + return TextStyle( + fontSize: 13, + color: SecurityThemeColors.labelGrey(context), + ); + } + + /// Returns the button text style. + + static const button = TextStyle( + fontSize: 14, + color: Colors.white, + ); + + /// Returns the cancel button style based on the current theme. + + static TextStyle cancelButton(BuildContext context) { + return TextStyle( + fontSize: 14, + color: SecurityThemeColors.text(context), + ); + } +} + +/// Text styles used for permission form. + +class RecipientTextStyle { + /// Style for the label. + + static const label = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ); + + /// Style for the WebID display. + + static const webId = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.blueAccent, + ); +} + +/// Layout constants for sub headings. + +class SubHeadingStyle { + /// Font size. + + static const double fontsize = 17.0; + + /// Font colour. + + static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); + + /// Font weight. + + static const FontWeight fontweight = FontWeight.bold; + + /// Padding. + + static const double padding = 8.0; +} + +/// Layout constants for headings. + +class HeadingStyle { + /// Font size. + + static const double fontsize = 22.0; + + /// Font colour. + + static const Color fontcolor = Color.fromRGBO(96, 125, 139, 1); + + /// Font weight. + + static const FontWeight fontweight = FontWeight.bold; + + /// Padding. + + static const double padding = 8.0; +} diff --git a/lib/src/constants/ui_window.dart b/lib/src/constants/ui_window.dart new file mode 100644 index 0000000..408d2ac --- /dev/null +++ b/lib/src/constants/ui_window.dart @@ -0,0 +1,99 @@ +/// Window and list item size constants for responsive UI. +/// +/// Copyright (C) 2025-2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang, Jess Moore, Tony Chen + +library; + +import 'package:flutter/material.dart'; + +/// Thresholds for window size. + +class WindowSize { + /// Small width threshold. + + static const double smallWidthLimit = 600; + + /// Small height threshold. + + static const double smallHeightLimit = 600; + + /// Boolean describing whether the parent widget is narrow. + /// Derived from the box constraints found by LayoutBuilder(). + /// + /// Arguments: + /// - [constraints] - The box constraints of the parent widget + /// where LayoutBuilder() was called. + + bool isNarrowWindow(BoxConstraints constraints) { + final bool isNarrow; + if (constraints.maxWidth < WindowSize.smallWidthLimit) { + isNarrow = true; + } else { + isNarrow = false; + } + + return isNarrow; + } +} + +/// Approximate size for grid items used for displaying text in list item. + +class ListItemSize { + /// Approximate height of compressed item in list when list item text + /// is line wrapped in a narrow mobile phone size window. + /// (Where each of note title, created date time, modified date time + /// are line wrapped to two lines.) + + static const double compressedItemHeight = 260; + + /// Approximate height of uncompressed item in list when list item text + /// is not line wrapped. + + static const double uncompressedItemHeight = 138; + + /// Calculate card aspect ratio to use for gridview builder cards + /// using the box constraints found by LayoutBuilder(). + /// + /// Arguments: + /// - [constraints] - The box constraints of the parent widget + /// where LayoutBuilder() was called. + + double calculateCardAspectRatio(BoxConstraints constraints) { + /// Aspect ratio (width / height) for gridview cards to display note items. + + final double cardAspectRatio; + + // Derive card aspect ratio (width / height). + + if (constraints.maxWidth < WindowSize.smallWidthLimit) { + cardAspectRatio = constraints.maxWidth / compressedItemHeight; + } else { + cardAspectRatio = constraints.maxWidth / uncompressedItemHeight; + } + return cardAspectRatio; + } +} diff --git a/lib/src/widgets/solid_logout_dialog.dart b/lib/src/widgets/solid_logout_dialog.dart index a235970..e39a4ad 100644 --- a/lib/src/widgets/solid_logout_dialog.dart +++ b/lib/src/widgets/solid_logout_dialog.dart @@ -143,8 +143,8 @@ Future logoutPopup( await showDialog( context: context, builder: (context) => LogoutDialog( - child: child, onLogoutSuccess: onLogoutSuccess, + child: child, ), ); }