diff --git a/README.md b/README.md
index a1d45b2..2663324 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ utilising the solidui package.
- [Requirements](#requirements)
- [SolidScaffold](#solidscaffold)
- [SolidFile](#solidfile)
+- [Grant Permission UI Example](#grant-permission-ui-example)
+- [View Permission UI Example](#view-permission-ui-example)
- [Authentication and Login Detection](#authentication-and-login-detection)
- [Security Key Management](#security-key-management)
- [API Reference](#api-reference)
@@ -115,23 +117,22 @@ Fine tune to suit the theme of the app:
- For defining specific access mode types or recipient types, use
optional parameters `accessModeList` and `recipientTypeList`.
-Granting permission (currently ublished in solidpod):
+Granting permission:
+ alt="Grant Permission" width="400">
Revoking permission:
+ alt="Revoke Permission" width="400">
- SharedResourcesUi widget displays
- resources shared with a Pod by others (currently published in
- solidpod):
+ resources shared with a Pod by others:
Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const GrantPermissionUi(
+ child: ReturnPage(),
+ ),
+ ),
+ ),
+)
+```
+
+### Permissions for a specific file
+
+```dart
+ElevatedButton(
+ child: const Text('Add/Delete Permissions from a Specific File'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const GrantPermissionUi(
+ resourceName: 'my-data-file.ttl',
+ child: ReturnPage(),
+ ),
+ ),
+ ),
+)
+```
+
+### Permissions for a specific directory
+
+```dart
+ElevatedButton(
+ child: const Text('Add/Delete Permissions from a Specific Directory'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const GrantPermissionUi(
+ resourceName: 'parentDir/',
+ child: ReturnPage(),
+ isFile: false,
+ ),
+ ),
+ ),
+)
+```
+
+### Permissions for an externally owned resource
+
+When the user has *control* access to a resource owned by someone else:
+
+```dart
+ElevatedButton(
+ child: const Text('Add/Delete Permissions from an External File'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => GrantPermissionUi(
+ resourceName: 'my-data-file.ttl',
+ isExternalRes: true,
+ ownerWebId: ownerWebId,
+ granterWebId: granterWebId,
+ child: ReturnPage(),
+ ),
+ ),
+ ),
+)
+```
+
+## View Permission UI Example
+
+The `SharedResourcesUi` widget displays the resources that have been
+shared with the current user's POD by others.
+
+### View all shared resources
+
+```dart
+ElevatedButton(
+ child: const Text('View Resources your WebID have access to'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const SharedResourcesUi(
+ child: ReturnPage(),
+ ),
+ ),
+ ),
+)
+```
+
+### View a specific shared resource
+
+```dart
+ElevatedButton(
+ child: const Text('View access to specific Resource'),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const SharedResourcesUi(
+ fileName: 'my-data-file.ttl',
+ sourceWebId:
+ 'https://pods.solidcommunity.au/john-doe/profile/card#me',
+ child: ReturnPage(),
+ ),
+ ),
+ ),
+)
+```
+
## Authentication and Login Detection
SolidUI provides dynamic login status detection and management through
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index dd5aacf..c04f535 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -21,7 +21,7 @@ dependency_overrides:
solidpod:
git:
url: https://github.com/anusii/solidpod.git
- ref: dev
+ ref: tony/59_ui_migration
solidui:
path: ..
diff --git a/lib/solidui.dart b/lib/solidui.dart
index 91d8cf4..50e80af 100644
--- a/lib/solidui.dart
+++ b/lib/solidui.dart
@@ -99,6 +99,7 @@ export 'src/models/snackbar_config.dart';
export 'src/utils/file_operations.dart';
export 'src/utils/is_text_file.dart';
export 'src/utils/solid_file_operations.dart';
+export 'src/utils/is_phone.dart';
export 'src/utils/solid_alert.dart';
export 'src/utils/solid_notifications.dart';
export 'src/utils/solid_pod_helpers.dart'
@@ -108,6 +109,26 @@ export 'src/widgets/solid_format_info_card.dart';
export 'src/widgets/build_message_container.dart';
+export 'src/widgets/app_bar.dart';
+export 'src/widgets/file_explorer.dart';
+export 'src/widgets/group_webid_input.dart';
+export 'src/widgets/ind_webid_input.dart';
+export 'src/widgets/ind_webid_input_screen.dart';
+export 'src/widgets/permission_checkbox.dart';
+export 'src/widgets/shared_resources_table.dart';
+
+export 'src/widgets/grant_permission_ui.dart';
+export 'src/widgets/shared_resources_ui.dart';
+export 'src/widgets/permission_table.dart';
+export 'src/widgets/grant_permission_form.dart';
+export 'src/widgets/select_recipients.dart';
+export 'src/widgets/show_selected_recipients.dart';
+export 'src/widgets/revoke_permission_button.dart';
+export 'src/widgets/share_resource_button.dart';
+
+export 'src/widgets/permission_history.dart';
+export 'src/widgets/grant_permission_helpers_ui.dart';
+
export 'src/constants/initial_setup.dart';
export 'src/screens/initial_setup_screen.dart';
export 'src/screens/initial_setup_screen_body.dart';
diff --git a/lib/src/utils/is_phone.dart b/lib/src/utils/is_phone.dart
new file mode 100644
index 0000000..9a2ab73
--- /dev/null
+++ b/lib/src/utils/is_phone.dart
@@ -0,0 +1,49 @@
+/// Check if we are running on a mobile device (and not a browser).
+///
+// Time-stamp:
+///
+/// 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: Jess Moore
+
+library;
+
+import 'package:flutter/foundation.dart'
+ show kIsWeb, defaultTargetPlatform, TargetPlatform;
+
+/// Checks the platform type to determine whether running on
+/// a mobile device.
+///
+/// Returns true if running on iOS or Android.
+/// Returns false on Flutter Web or desktop platforms.
+
+bool isPhone() {
+ if (kIsWeb) {
+ return false;
+ }
+
+ return defaultTargetPlatform == TargetPlatform.iOS ||
+ defaultTargetPlatform == TargetPlatform.android;
+}
diff --git a/lib/src/widgets/app_bar.dart b/lib/src/widgets/app_bar.dart
new file mode 100644
index 0000000..72228ed
--- /dev/null
+++ b/lib/src/widgets/app_bar.dart
@@ -0,0 +1,75 @@
+/// A default app bar.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage, Ashley Tang
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidui/src/widgets/solid_login_helper.dart'
+ show pushReplacement;
+
+/// A default app bar that is used when user does not define an app bar for
+/// the UI
+PreferredSizeWidget defaultAppBar(
+ BuildContext context,
+ String title,
+ Color backgroundColor,
+ Widget child, {
+ VoidCallback? onNavigateBack,
+ bool Function()? getResult,
+}) {
+ return AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Colors.black),
+ onPressed: () {
+ // Call the callback if provided.
+
+ onNavigateBack?.call();
+
+ if (getResult != null) {
+ // Pop with result from callback.
+
+ Navigator.pop(context, getResult());
+ } else {
+ // Use the original pushReplacement behaviour.
+
+ pushReplacement(context, child);
+
+ // Navigator.pushReplacement(
+ // context,
+ // MaterialPageRoute(builder: (context) => child),
+ // );
+ }
+ },
+ ),
+ backgroundColor: backgroundColor,
+ title: Text(title),
+ );
+}
diff --git a/lib/src/widgets/file_explorer.dart b/lib/src/widgets/file_explorer.dart
new file mode 100644
index 0000000..b6f4925
--- /dev/null
+++ b/lib/src/widgets/file_explorer.dart
@@ -0,0 +1,430 @@
+/// A simple file explorer for navigating through both own and external PODs
+///
+/// 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: Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/solidui.dart' show normalLoadingScreenHeight;
+import 'package:solidui/src/utils/snack_bar.dart';
+import 'package:solidui/src/utils/solid_alert.dart';
+import 'package:solidui/src/widgets/solid_loading_screen.dart';
+
+/// A simple file explorer class with two input parameters
+class FileExplorerScreen extends StatefulWidget {
+ const FileExplorerScreen({
+ super.key,
+ required this.folderPath,
+ required this.child,
+ required this.isEditable,
+ required this.ownerWebId,
+ });
+
+ @override
+ State createState() => _FileExplorerScreenState();
+
+ final String folderPath;
+ final Widget child;
+ final bool isEditable;
+ final String ownerWebId;
+}
+
+class _FileExplorerScreenState extends State {
+ String currentPath = '';
+ List folderList = [];
+ List fileList = [];
+
+ bool isLoading = true;
+ String errMsg = '';
+
+ @override
+ void initState() {
+ super.initState();
+ _getResources();
+ }
+
+ Future _getResources() async {
+ try {
+ final res = await getResourcesInContainer(widget.folderPath);
+ setState(() {
+ folderList = res.subDirs;
+ fileList = res.files;
+ currentPath = widget.folderPath;
+ isLoading = false;
+ });
+ } on AccessForbiddenException catch (e) {
+ debugPrint('Exception occured: $e');
+ setState(() {
+ errMsg = 'You do not have access to this directory';
+ isLoading = false;
+ });
+ } on AccessFailedException catch (e) {
+ debugPrint('Exception occured: $e');
+ setState(() {
+ errMsg = 'Reading directory content failed. Please try again';
+ isLoading = false;
+ });
+ } catch (e) {
+ debugPrint('Unknown exception occured: $e');
+ setState(() {
+ errMsg = 'Unkown error occured: $e';
+ isLoading = false;
+ });
+ }
+ }
+
+ PreferredSizeWidget defaltAppBar() {
+ return AppBar(
+ title: const Text('Go back'),
+ leading: currentPath.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () {
+ // final parent = Directory(currentPath).parent;
+ // _loadDirectory(parent.path);
+
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(builder: (context) => widget.child),
+ );
+ },
+ )
+ : null,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (isLoading) {
+ return Scaffold(body: loadingScreen(normalLoadingScreenHeight));
+ }
+
+ if (errMsg.isNotEmpty) {
+ return Scaffold(
+ appBar: defaltAppBar(),
+ body: Center(
+ child: Text(
+ errMsg,
+ style: const TextStyle(fontSize: 16, color: Colors.grey),
+ ),
+ ),
+ );
+ }
+
+ final List<_Section> sections = [];
+
+ if (folderList.isNotEmpty) {
+ sections.add(
+ _Section(title: 'Folders', items: folderList, isFolder: true),
+ );
+ }
+
+ if (fileList.isNotEmpty) {
+ sections.add(_Section(title: 'Files', items: fileList, isFolder: false));
+ }
+
+ // If both are empty, show placeholder
+ if (sections.isEmpty) {
+ return Scaffold(
+ appBar: defaltAppBar(),
+ body: const Center(
+ child: Text(
+ 'No directories or files found.',
+ style: TextStyle(fontSize: 16, color: Colors.grey),
+ ),
+ ),
+ );
+ }
+
+ return Scaffold(
+ appBar: defaltAppBar(),
+ body: // One scrollable list
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: double.infinity,
+ color: Colors.grey[200],
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Row(
+ children: [
+ const Icon(Icons.folder_open, color: Colors.blue),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ currentPath,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ overflow: TextOverflow.ellipsis, // truncate if long
+ ),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child: ListView.builder(
+ itemCount: sections.length,
+ itemBuilder: (context, sectionIndex) {
+ final section = sections[sectionIndex];
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Section header
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ section.title,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ // List items
+ ListView.builder(
+ itemCount: section.items.length,
+ shrinkWrap: true,
+ physics:
+ const NeverScrollableScrollPhysics(), // Disable inner scroll
+ itemBuilder: (context, index) {
+ final item = section.items[index];
+ return ListTile(
+ leading: Icon(
+ section.isFolder
+ ? Icons.folder
+ : Icons.insert_drive_file,
+ color:
+ section.isFolder ? Colors.amber : Colors.blue,
+ ),
+ trailing: (!section.isFolder && widget.isEditable)
+ ? IconButton(
+ icon: const Icon(Icons.edit),
+ onPressed: () async {
+ final filePath =
+ '${widget.folderPath}$item';
+ final fileContent = await readExternalPod(
+ filePath,
+ );
+
+ final TextEditingController editController =
+ TextEditingController(
+ text: fileContent,
+ );
+
+ if (!context.mounted) return;
+ await showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text(
+ 'Edit File Content',
+ ),
+ content: SizedBox(
+ width: double.maxFinite,
+ child: TextField(
+ controller: editController,
+ autofocus: true,
+ maxLines:
+ null, // allows multiple lines
+ minLines:
+ 3, // starts with 3 visible lines
+ textInputAction:
+ TextInputAction.newline,
+ keyboardType:
+ TextInputType.multiline,
+ decoration: const InputDecoration(
+ labelText:
+ 'Enter your file content',
+ border: OutlineInputBorder(),
+ alignLabelWithHint: true,
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(
+ context,
+ ).pop(); // Cancel -> close dialog
+ },
+ child: const Text('Cancel'),
+ ),
+ ElevatedButton(
+ onPressed: () async {
+ final newContent =
+ editController.text;
+ // Check if the content has changed
+ if (newContent != fileContent) {
+ try {
+ await writeExternalPod(
+ filePath,
+ newContent,
+ widget.ownerWebId,
+ );
+
+ if (!context.mounted) {
+ return;
+ }
+
+ showSnackBar(
+ context,
+ 'Changes saved successfully!',
+ Colors.green,
+ );
+ } on Object catch (e, trace) {
+ debugPrint('$e');
+ debugPrint('$trace');
+ if (!context.mounted) {
+ return;
+ }
+ showSnackBar(
+ context,
+ 'Something went wrong! Please try again.',
+ Colors.red,
+ );
+ }
+ } else {
+ if (!context.mounted) return;
+ showSnackBar(
+ context,
+ 'Content has not changed',
+ Colors.orange,
+ );
+ }
+
+ Navigator.of(
+ context,
+ ).pop(); // Save -> return value
+ },
+ child: const Text('Save'),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ )
+ : null,
+ title: Text(item),
+ onTap: () async {
+ if (section.isFolder) {
+ final newFolderPath =
+ '${widget.folderPath}$item/';
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => FileExplorerScreen(
+ folderPath: newFolderPath,
+ isEditable: widget.isEditable,
+ ownerWebId: widget.ownerWebId,
+ child: FileExplorerScreen(
+ folderPath: widget.folderPath,
+ isEditable: widget.isEditable,
+ ownerWebId: widget.ownerWebId,
+ child: widget.child,
+ ),
+ ),
+ ),
+ );
+ } else {
+ final filePath = '${widget.folderPath}$item';
+
+ try {
+ final fileContent = await readExternalPod(
+ filePath,
+ );
+
+ if (!context.mounted) return;
+ await showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: const Text('File content'),
+ content: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ width: double.infinity,
+ height: 300,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(
+ 15,
+ ),
+ ),
+ child: Text(fileContent),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ // Close the dialog
+ Navigator.of(ctx).pop();
+ },
+ child: const Text('Ok'),
+ ),
+ ],
+ ),
+ );
+ } on Object catch (e, trace) {
+ debugPrint(e.toString());
+ debugPrint(trace.toString());
+ if (!context.mounted) return;
+ await alert(
+ context,
+ 'The file $item could not be found!',
+ );
+ }
+ }
+ // handle open file/folder
+ },
+ );
+ },
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// Helper class for sections
+class _Section {
+ final String title;
+ final List items;
+ final bool isFolder;
+
+ _Section({required this.title, required this.items, required this.isFolder});
+}
diff --git a/lib/src/widgets/grant_permission_form.dart b/lib/src/widgets/grant_permission_form.dart
new file mode 100644
index 0000000..a1fc611
--- /dev/null
+++ b/lib/src/widgets/grant_permission_form.dart
@@ -0,0 +1,435 @@
+/// A button for sharing a resource.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore, Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/solidui.dart'
+ show
+ ActionColors,
+ GrantPermFormLayout,
+ debugPrintException,
+ debugPrintFailure,
+ failureMsg,
+ getPermissionCheckBoxes,
+ isPhone,
+ makeSubHeading,
+ podNotInitMsg,
+ smallGapV,
+ successMsg,
+ updatePermissionMsg;
+import 'package:solidui/src/utils/snack_bar.dart';
+import 'package:solidui/src/utils/solid_alert.dart';
+import 'package:solidui/src/widgets/group_webid_input.dart';
+import 'package:solidui/src/widgets/ind_webid_input_screen.dart';
+import 'package:solidui/src/widgets/select_recipients.dart';
+import 'package:solidui/src/widgets/show_selected_recipients.dart';
+
+/// Sharing (grant permission) form dialog function
+///
+/// A [StatefulWidget] for creating a grant permission form
+/// dialog to get recipient and access modes to grant for the
+/// provided [resourceName]
+///
+/// Parameters:
+/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource.
+/// - [isExternalRes] - Boolean flag describing whether the resource
+/// is externally owned.
+/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned.
+/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned.
+/// - [accessModeList] - List of access mode options to show.
+/// - [recipientTypeList] - List of recipient type options to show.
+/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory.
+/// - [updatePermissionsFunction] is the function to be called to refresh the permission table.
+/// - [updatePermissionGrantedFunction] - is the function to be called
+/// when permissions are granted successfully
+/// - [onPermissionGranted] - Callback function called when permissions are granted successfully.
+
+class GrantPermissionForm extends StatefulWidget {
+ /// String to assign the webId of the resource owner.
+
+ final String ownerWebId;
+
+ /// String to assign the external webId of the resource granter.
+
+ final String granterWebId;
+
+ /// The name of the file or directory that access is being granted for.
+
+ final String resourceName;
+
+ final bool isExternalRes;
+
+ /// A flag to determine whether the given resource is a file or not.
+
+ final bool isFile;
+
+ /// The list of access modes to show in form. By default
+ /// all four types of access mode are listed.
+
+ final List accessModeList;
+
+ /// The list of types of recipients to show in form. By default
+ /// all four types of recipient are listed.
+
+ final List recipientTypeList;
+
+ /// Map of data files on a user's POD used to extract the
+ /// user's recipient list by the WebIdTextInputScreen.
+ /// If not provided, the WebIdTextInputScreen will read the
+ /// user's files in their app data folder on their Pod to
+ /// fetch the ACLs needed to derive the user's recipient list.
+
+ final Map dataFilesMap;
+
+ /// Function run to update permissions table
+
+ final Function updatePermissionsFunction;
+
+ /// Function when permissions are granted successfully
+
+ final Function updatePermissionGrantedFunction;
+
+ /// Callback function called when permissions are granted successfully.
+
+ final VoidCallback? onPermissionGranted;
+
+ const GrantPermissionForm({
+ super.key,
+ required this.updatePermissionsFunction,
+ required this.resourceName,
+ required this.ownerWebId,
+ required this.granterWebId,
+ this.accessModeList = const ['read', 'write', 'append', 'control'],
+ this.recipientTypeList = const ['public', 'indi', 'auth', 'group'],
+ required this.isExternalRes,
+ required this.isFile,
+ required this.updatePermissionGrantedFunction,
+ this.dataFilesMap = const {},
+ this.onPermissionGranted,
+ });
+
+ @override
+ State createState() => _GrantPermissionFormState();
+}
+
+class _GrantPermissionFormState extends State {
+ /// Selected recipient
+
+ RecipientType selectedRecipientType = RecipientType.none;
+
+ /// Selected recipient details
+
+ String selectedRecipientDetails = '';
+
+ /// List of webIds for group permission
+
+ List finalWebIdList = [];
+
+ /// Selected group name
+
+ String selectedGroupName = '';
+
+ /// Selected list of permissions
+
+ List selectedPermList = [];
+
+ /// Flag to track if permissions were granted successfully.
+
+ bool permissionsGrantedSuccessfully = false;
+
+ /// read permission checked flag
+
+ bool readChecked = false;
+
+ /// write permission checked flag
+
+ bool writeChecked = false;
+
+ /// control permission checked flag
+
+ bool controlChecked = false;
+
+ /// append permission checked flag
+
+ bool appendChecked = false;
+
+ /// Public permission check flag
+
+ bool publicChecked = false;
+
+ /// Define access mode list
+
+ List accessModeList = [];
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Load access mode list to be displayed
+ for (final accessModeStr in widget.accessModeList) {
+ accessModeList.add(getAccessMode(accessModeStr));
+ }
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ /// Private function to call alert dialog in share resource button
+ /// context. This provides an alert dialog over the top of the
+ /// grant permission form dialog.
+ Future _alert(String msg) async => alert(context, msg);
+
+ /// Private function to show snackbar in share resource button context
+ Future _showSnackBar(
+ String msg,
+ Color bgColor, {
+ Duration duration = const Duration(seconds: 4),
+ }) async =>
+ showSnackBar(context, msg, bgColor, duration: duration);
+
+ /// Update selected webid list with individual recipient webid
+ /// [receiverWebId].
+ void updateIndWebIdInput(String receiverWebId) => setState(() {
+ selectedRecipientDetails = receiverWebId;
+ finalWebIdList = [receiverWebId];
+ });
+
+ /// Update selected webid list with list of webids in
+ /// recipient group [webIdList] and their group name
+ /// [groupName].
+
+ void updateGroupWebIdInput(String groupName, List webIdList) =>
+ setState(() {
+ selectedRecipientDetails = webIdList.join(', ');
+ finalWebIdList = webIdList;
+ selectedGroupName = groupName;
+ });
+
+ /// Update checked status of access mode boxes to show
+ /// selected access modes.
+ void updateCheckbox(bool newValue, AccessMode accessMode) => setState(() {
+ switch (accessMode) {
+ case AccessMode.read:
+ readChecked = newValue;
+ case AccessMode.write:
+ writeChecked = newValue;
+ case AccessMode.control:
+ controlChecked = newValue;
+ case AccessMode.append:
+ appendChecked = newValue;
+ }
+ if (newValue) {
+ selectedPermList.add(accessMode.mode);
+ } else {
+ selectedPermList.remove(accessMode.mode);
+ }
+ });
+
+ /// Define button click actions for each recipient type button
+
+ /// Set recipients to public
+ void _setRecipientsToPublic() => setState(() {
+ selectedRecipientType = RecipientType.public;
+ selectedRecipientDetails = 'Anyone (release publicly)';
+ finalWebIdList = [publicAgent.value];
+ });
+
+ /// Set recipients to authorised users
+ void _setRecipientsToAuthUsers() => setState(() {
+ selectedRecipientType = RecipientType.authUser;
+ selectedRecipientDetails =
+ 'Authenticated Users (any user logged in with their webId)';
+ finalWebIdList = [authenticatedAgent.value];
+ });
+
+ /// Select individual recipient
+ void _setRecipientsToIndividual() => setState(() {
+ selectedRecipientType = RecipientType.individual;
+ });
+
+ /// Select a group of recipients
+ void _setRecipientsToGroup() => setState(() {
+ selectedRecipientType = RecipientType.group;
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ insetPadding: GrantPermFormLayout.contentPadding,
+ title: Text('Share ${widget.resourceName}'),
+ content: Scrollbar(
+ thumbVisibility: true,
+ child: SingleChildScrollView(
+ primary: true,
+ child: SizedBox(
+ // Use full width on phones, else use a preset narrower width
+ width: (!isPhone())
+ ? GrantPermFormLayout.dialogWidth
+ : double.maxFinite,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ makeSubHeading('Select the recipient/s of file access'),
+
+ // Show Select Recipient Buttons
+ SelectRecipients(
+ isExternalRes: widget.isExternalRes,
+ recipientTypeList: widget.recipientTypeList,
+ setPublicFunction: _setRecipientsToPublic,
+ setAuthUsersFunction: _setRecipientsToAuthUsers,
+ setIndividualFunction: _setRecipientsToIndividual,
+ setGroupFunction: _setRecipientsToGroup,
+ ),
+
+ // Select Individual recipient if required
+ if (selectedRecipientType == RecipientType.individual) ...[
+ IndWebIdInputScreen(
+ onSubmitFunction: updateIndWebIdInput,
+ dataFilesMap: widget.dataFilesMap,
+ ),
+ ] else if (selectedRecipientType == RecipientType.group) ...[
+ // Select group of recipients if required
+ GroupWebIdTextInput(onSubmitFunction: updateGroupWebIdInput),
+ ],
+ // List selected recipient webids or recipient
+ // type (public/auth)
+ ShowSelectedRecipients(
+ selectedRecipientType: selectedRecipientType,
+ selectedRecipientDetails: selectedRecipientDetails,
+ selectedGroupName: selectedGroupName,
+ ),
+ smallGapV,
+ makeSubHeading('Select one or more file access permissions'),
+ // Show access mode checkboxes and update
+ // selection status on click
+ ...getPermissionCheckBoxes(
+ accessModeList,
+ modeSwitches: {
+ AccessMode.read: readChecked,
+ AccessMode.write: writeChecked,
+ AccessMode.control: controlChecked,
+ AccessMode.append: appendChecked,
+ },
+ onUpdate: updateCheckbox,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () async {
+ // Grant Permission and update permission map
+ // used by permission table
+
+ if (selectedRecipientType.type.isNotEmpty) {
+ if (selectedPermList.isNotEmpty) {
+ SolidFunctionCallStatus result;
+ try {
+ // Update ACL and permission logs to grant permission
+ result = await grantPermission(
+ fileName: widget.resourceName,
+ isFile: widget.isFile,
+ permissionList: selectedPermList,
+ recipientType: selectedRecipientType,
+ recipientWebIdList: finalWebIdList,
+ ownerWebId: widget.ownerWebId,
+ granterWebId: widget.granterWebId,
+ isExternalRes: widget.isExternalRes,
+ groupName: selectedGroupName,
+ );
+
+ // Close grant permission dialog
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ } on Object catch (e, stackTrace) {
+ result = SolidFunctionCallStatus.fail;
+ debugPrintException(e, stackTrace);
+ }
+
+ if (result == SolidFunctionCallStatus.success) {
+ _showSnackBar(successMsg, ActionColors.success);
+ // Update permissions table
+ await widget.updatePermissionsFunction(
+ widget.resourceName, //_resourceName,
+ isFile: widget.isFile,
+ isExternalRes: widget.isExternalRes,
+ );
+
+ // Mark permissions as granted successfully for callback tracking
+ await widget.updatePermissionGrantedFunction();
+
+ // Trigger the onPermissionGranted callback if provided
+ widget.onPermissionGranted?.call();
+ } else if (result == SolidFunctionCallStatus.fail) {
+ // More detailed error message with troubleshooting tips
+ _showSnackBar(failureMsg, ActionColors.error);
+
+ // Also log to console for debugging
+ debugPrintFailure(
+ widget.resourceName, // _resourceName,
+ finalWebIdList,
+ selectedPermList,
+ );
+ } else if (result == SolidFunctionCallStatus.notInitialised) {
+ _showSnackBar(podNotInitMsg, ActionColors.warning);
+ } else {
+ await _alert(updatePermissionMsg);
+ }
+ } else {
+ await _alert(
+ 'Please select one or more file access permissions',
+ );
+ }
+ } else {
+ await _alert('Please select a type of recipient');
+ }
+ },
+ child: const Text('Grant Permission'),
+ ),
+ TextButton(
+ onPressed: () {
+ // Close dialog
+ Navigator.of(context).pop();
+ },
+ child: const Text('Cancel'),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/widgets/grant_permission_helpers_ui.dart b/lib/src/widgets/grant_permission_helpers_ui.dart
new file mode 100644
index 0000000..73cb212
--- /dev/null
+++ b/lib/src/widgets/grant_permission_helpers_ui.dart
@@ -0,0 +1,184 @@
+/// UI helper functions and constants for the grant permission workflow.
+///
+/// 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: Anushka Vidanage, Jess Moore, Ashley Tang, Dawei Chen
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart' show AccessMode, RecipientType;
+
+import 'package:solidui/src/constants/ui_layout.dart' show SharingPageLayout;
+import 'package:solidui/src/widgets/permission_checkbox.dart';
+
+// Constants
+
+/// Tooltip strings for each recipient type shown in the sharing UI.
+
+const recipientToolTips = {
+ RecipientType.public: '''
+ **Public:** This file will be publicly
+ accessible so that even users without a
+ Data Vault can access the file.
+ ''',
+ RecipientType.authUser: '''
+**Users:** The file will be available to
+any user who has registered a Data
+Vault. When they have logged into their
+Data Vault they will be able to access
+the file.
+''',
+ RecipientType.individual: '''
+**Individual:** The file will be available
+only to the identified individual user. A
+WebID is required to identify the
+individual who is gratned access to the
+file.
+''',
+ RecipientType.group: '''
+**Group:** A collection of WebIDs can be
+provided so that as a group they can
+access the file.
+''',
+};
+
+const updatePermissionMsg =
+ 'Please login first to update file access permission';
+const podNotInitMsg =
+ 'The owner of one or more WebIds you entered have not initialised their PODs yet! They need to login and setup their POD first.';
+const noAclMsg = 'Resource does not have a corresponding ACL file.\n'
+ 'If the ACL is inherited, provide parent directory as the resource name!';
+const successMsg = 'File access permissions granted successfully!';
+const failureMsg =
+ 'Permission granting failed. Check console logs for details. Common issues: resource not found, invalid WebID format, or network connectivity.';
+
+// Debug helpers
+
+String getFailureMsg(String fileName) =>
+ '❌ [GrantPermissionUI] Permission granting failed for file: $fileName';
+
+String getRecipientMsg(List? finalWebIdList) =>
+ '🎯 [GrantPermissionUI] Recipients: $finalWebIdList';
+
+String getPermissionMsg(List permissionList) =>
+ '🔐 [GrantPermissionUI] Permissions: $permissionList';
+
+String getExceptionMsg(Object e) =>
+ '💥 [GrantPermissionUI] Exception in grantPermission: $e';
+
+String getStackTraceMsg(StackTrace stackTrace) =>
+ '📚 [GrantPermissionUI] Stack trace: $stackTrace';
+
+void debugPrintException(Object e, StackTrace stackTrace) {
+ debugPrint(getExceptionMsg(e));
+ debugPrint(getStackTraceMsg(stackTrace));
+}
+
+void debugPrintFailure(
+ String fileName,
+ List? finalWebIdList,
+ List permissionList,
+) {
+ debugPrint(getFailureMsg(fileName));
+ debugPrint(getRecipientMsg(finalWebIdList));
+ debugPrint(getPermissionMsg(permissionList));
+}
+
+// Recipient type lists
+
+/// Relevant recipients types for resource sharing by the resource owner.
+const ownerRecipientTypes = [
+ RecipientType.public,
+ RecipientType.authUser,
+ RecipientType.individual,
+ RecipientType.group,
+];
+
+/// Relevant recipient types for resource sharing by the resource granter
+/// (i.e. an entity with control access).
+const granterRecipientTypes = [RecipientType.individual, RecipientType.group];
+
+/// Get title of sharing page.
+String makeSharingTitleStr({String? fileName, bool isFile = false}) =>
+ fileName != null
+ ? isFile
+ ? 'Share $fileName'
+ : 'Share $fileName folder'
+ : 'Share your data with other user\'s PODs';
+
+// Widget builders
+
+/// Build a list of permission check-box widgets for the given [accessModes].
+
+List getPermissionCheckBoxes(
+ List accessModes, {
+ required Map modeSwitches,
+ required Function onUpdate,
+}) =>
+ [
+ for (final mode in AccessMode.getAllModes())
+ if (accessModes.contains(mode))
+ permissionCheckbox(mode, modeSwitches[mode]!, onUpdate),
+ ];
+
+/// Build a resource form widget with a text field and a file/directory toggle.
+
+Widget getResourceForm({
+ required TextEditingController formController,
+ required bool isFile,
+ required void Function(bool) onResourceTypeChange,
+}) =>
+ Padding(
+ padding: SharingPageLayout.inputPadding,
+ child: Column(
+ children: [
+ TextFormField(
+ controller: formController,
+ decoration: const InputDecoration(
+ hintText:
+ 'Data file path (inside your data folder, Eg: personal/about.ttl)',
+ ),
+ validator: (value) =>
+ (value == null || value.isEmpty) ? 'Empty field' : null,
+ ),
+ const SizedBox(height: 10),
+ SwitchListTile(
+ title: const Text(
+ 'Is a File?',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ subtitle: Text(isFile ? 'Yes' : 'No'),
+ value: isFile,
+ onChanged: onResourceTypeChange,
+ thumbColor: WidgetStateProperty.resolveWith(
+ (Set states) =>
+ states.contains(WidgetState.selected) ? Colors.green : null,
+ ),
+ ),
+ ],
+ ),
+ );
diff --git a/lib/src/widgets/grant_permission_ui.dart b/lib/src/widgets/grant_permission_ui.dart
new file mode 100644
index 0000000..f2391c3
--- /dev/null
+++ b/lib/src/widgets/grant_permission_ui.dart
@@ -0,0 +1,655 @@
+// A screen to demonstrate the data sharing capabilities of PODs.
+///
+// Time-stamp:
+///
+/// Copyright (C) 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: Anushka Vidanage, Jess Moore, Ashley Tang, Dawei Chen
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/solidui.dart';
+
+/// A [StatefulWidget] for showing and editing access permissions to a
+/// resource. It displays the permission table of users with access, and
+/// allows the user to change access permissions: by granting access
+/// to others, changing a recipients access permissions or revoking
+/// access permissions.
+///
+/// Parameters:
+/// - [child] - the child widget to return to.
+/// - [title] - Page title to show in the app bar.
+/// - [backgroundColor] - Background color.
+/// - [showAppBar] - Boolean flag describing whether to show app bar.
+/// - [isExternalRes] - Boolean flag describing whether the resource
+/// is externally owned.
+/// - [accessModeList] - List of access mode options to show.
+/// - [recipientTypeList] - List of recipient type options to show.
+/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned.
+/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externall owned.
+/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource.
+/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory.
+/// - [customAppBar] - Specify a custom app bar widget.
+/// - [onPermissionGranted] - Callback function called when permissions are granted successfully.
+/// - [onNavigateBack] - Callback function called when navigating back from the screen.
+
+class GrantPermissionUi extends StatefulWidget {
+ const GrantPermissionUi({
+ required this.child,
+ this.title = 'Demonstrating data sharing functionality',
+ this.backgroundColor = const Color.fromARGB(255, 210, 210, 210),
+ this.showAppBar = true,
+ this.isExternalRes = false,
+ this.accessModeList = const ['read', 'write', 'append', 'control'],
+ this.recipientTypeList = const ['public', 'indi', 'auth', 'group'],
+ this.ownerWebId,
+ this.granterWebId,
+ this.resourceName,
+ this.isFile = true,
+ this.dataFilesMap = const {},
+ this.customAppBar,
+ this.onPermissionGranted,
+ this.onNavigateBack,
+ super.key,
+ }) : assert(
+ // Requires ownerWebId if resource
+ // is an externally owned.
+ isExternalRes == false || ownerWebId != null,
+ 'ownerWebId must be provided if isExternalRes == true',
+ );
+
+ /// The child widget to return to when back button is pressed and/or when
+ /// page is reloaded after a permission is granted or revoked.
+
+ final Widget child;
+
+ /// The text appearing in the app bar.
+
+ final String title;
+
+ /// The text appearing in the app bar.
+
+ final Color backgroundColor;
+
+ /// The boolean to decide whether to display an app bar or not.
+
+ final bool showAppBar;
+
+ /// The boolean to decide whether the resources is from an external POD or not
+
+ final bool isExternalRes;
+
+ /// String to assign the webId of the resource owner. Must
+ /// be set if [isExternalRes] is set to true.
+
+ final String? ownerWebId;
+
+ /// String to assign the external webId of the resource granter. Must
+ /// be set if [isExternalRes] is set to true.
+
+ final String? granterWebId;
+
+ /// The list of access modes to be displayed. By default all four types of
+ /// access mode are listed.
+
+ final List accessModeList;
+
+ /// The list of types of recipients receiving permission to access the
+ /// resource. By default all four types of recipient are listed.
+
+ final List recipientTypeList;
+
+ /// The name of the file or directory permission is being set to. This is a
+ /// non required parameter. If not set there will be a text field to define
+ /// the file name. If [isExternalRes] is set to true this must be set and the
+ /// value should be the url of the resource.
+
+ final String? resourceName;
+
+ /// A flag to determine whether the given resource is a file or not. This is
+ /// a parameter with default value true. In the case where [resourceName] is
+ /// not set there will be a toggle to define this parameter.
+ /// If [isExternalRes] is set to true this must be set and the value should
+ /// be the url of the resource. Also if [resourceName] is set this flag must
+ /// also be set
+
+ final bool isFile;
+
+ /// Map of data files on a user's POD used to extract the
+ /// user's recipient list by the WebIdTextInputScreen.
+ /// If not provided, the WebIdTextInputScreen will read the
+ /// user's files in their app data folder on their Pod to
+ /// fetch the ACLs needed to derive the user's recipient list.
+
+ final Map dataFilesMap;
+
+ /// App specific app bar
+
+ final PreferredSizeWidget? customAppBar;
+
+ /// Callback function called when permissions are granted successfully.
+
+ final VoidCallback? onPermissionGranted;
+
+ /// Callback function called when navigating back from the screen.
+
+ final VoidCallback? onNavigateBack;
+
+ @override
+ GrantPermissionUiState createState() => GrantPermissionUiState();
+}
+
+/// Class to build a UI for granting permission to a given file
+
+class GrantPermissionUiState extends State
+ with SingleTickerProviderStateMixin {
+ /// Flag to check whether permission table is initialised.
+
+ bool permTableInitialied = false;
+
+ /// Flag to check whether permission history is initialised.
+
+ bool permHistoryInitialied = false;
+
+ /// Define access mode list
+
+ List accessModeList = [];
+
+ /// Define recipient type list
+
+ List recipientTypeList = [];
+
+ /// Filename text controller
+
+ final fileNameController = TextEditingController();
+
+ /// Permission data map of a file
+
+ Map permDataMap = {};
+
+ /// Owner WebId
+
+ String _ownerWebId = '';
+
+ /// Granter WebId
+
+ String _granterWebId = '';
+
+ /// File name of the current permission data map
+
+ String permDataFile = '';
+
+ /// Flag to track if permissions were granted successfully.
+
+ bool permissionsGrantedSuccessfully = false;
+
+ /// Pod data list retreived as a Future
+
+ late Future getACLPerm;
+
+ /// Permission history list retreived as a Future
+
+ late Future> getPermHistoryList;
+
+ /// Permission history list
+
+ List permHistoryList = [];
+
+ /// Unfiltered permission history list
+
+ List unFilteredPermHistoryList = [];
+
+ /// Flag to check whether permission history is initialised.
+
+ bool showCurrentPermOnly = false;
+
+ /// A flag to identify if the resource is a file or not
+ bool isFile = true;
+
+ /// Gets permission details data from ACL on POD server if necessary.
+
+ Future loadACLData(
+ String resName, {
+ bool isFile = true,
+ bool isExternalRes = false,
+ }) async {
+ final SolidFunctionCallStatus response = await chkExistsAndHasAcl(
+ fileName: resName,
+ isFile: isFile,
+ isExternalRes: isExternalRes,
+ );
+
+ switch (response) {
+ case SolidFunctionCallStatus.aclFound:
+
+ // Permission map from ACL of resource
+ final Map result = await readPermission(
+ fileName: resName,
+ isFile: isFile,
+ isExternalRes: isExternalRes,
+ );
+
+ // Permission Details object to store permission map from ACL, and owner
+ // and granter of a resource.
+
+ final permissionDetails = PermissionDetails(
+ permissionMap: result,
+ ownerWebId: await getAuthoriser(
+ isExternalRes: isExternalRes,
+ webId: widget.ownerWebId,
+ ),
+ granterWebId: await getAuthoriser(
+ isExternalRes: isExternalRes,
+ isGranter: true,
+ ),
+ );
+
+ return permissionDetails;
+
+ case SolidFunctionCallStatus.notLoggedIn:
+ await _alert('Please login first to retrieve permission');
+
+ case SolidFunctionCallStatus.noAclFound:
+ await _alert(noAclMsg);
+
+ default:
+ await _alert('Unknown error');
+ }
+
+ return null;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ // Load permission map from ACL, owner and granter web ids
+ if (widget.resourceName != null) {
+ getACLPerm = loadACLData(
+ widget.resourceName as String,
+ isFile: widget.isFile,
+ isExternalRes: widget.isExternalRes,
+ );
+ getPermHistoryList =
+ sharedResourcesHistory(resourceName: widget.resourceName as String);
+ // permHistoryList = [];
+ }
+ }
+
+ /// Update the permission data map
+
+ Future _updatePermissions(
+ String fileName, {
+ bool isFile = true,
+ bool isExternalRes = false,
+ }) async {
+ final pdata = await loadACLData(
+ fileName,
+ isFile: isFile,
+ isExternalRes: isExternalRes,
+ );
+ final updatedPermHistoryList =
+ await sharedResourcesHistory(resourceName: fileName);
+
+ assert(pdata != null);
+
+ if (pdata!.permissionMap.isEmpty) {
+ await _alert('We could not find a resource by the name $fileName');
+ } else {
+ setState(() {
+ permDataMap = pdata.permissionMap;
+ permDataFile = fileName;
+ _ownerWebId = pdata.ownerWebId;
+ _granterWebId = pdata.granterWebId;
+ });
+ }
+
+ if (updatedPermHistoryList.isEmpty) {
+ await _alert(
+ 'We could not find permission log entries for resource by the name $fileName',
+ );
+ } else {
+ setState(() {
+ permHistoryList = updatedPermHistoryList;
+ // Set full unfiltered list to current list from updated
+ // log fetch
+ unFilteredPermHistoryList = updatedPermHistoryList;
+ });
+ }
+ }
+
+ // Search log records
+ void _searchLogs(String enteredKeyword) {
+ bool found(it) => it.toLowerCase().contains(enteredKeyword.toLowerCase());
+
+ List results = [];
+ if (enteredKeyword.isEmpty) {
+ // Display all log records if no search string
+ results = unFilteredPermHistoryList;
+ // permHistoryList;
+ } else {
+ // Display log records with recipient name, granter name,
+ // permission type, permission matches
+ results = unFilteredPermHistoryList.where((item) {
+ return [
+ item.recipientName,
+ item.granterName,
+ item.permissionType,
+ item.permissionList,
+ ].map(found).any((result) => result);
+ }).toList();
+ }
+
+ // Refresh the UI
+ setState(() {
+ permHistoryList = results;
+ });
+ }
+
+ /// Filter log records for current/all log records
+ void getLatestLogRecords() {
+ List currentLogRecords = [];
+ List currentRecipients = [];
+
+ // Loop through logs and get the latest for each resource
+ for (final record in permHistoryList) {
+ // Store most recent grant record
+ if ((record.permissionType).contains('grant')) {
+ final recipientWebId = record.recipientWebId;
+
+ currentRecipients =
+ currentLogRecords.map((item) => item.recipientWebId).toList();
+
+ if (currentRecipients.contains(recipientWebId)) {
+ final int prevMatchIndex = currentLogRecords
+ .indexWhere((item) => item.recipientWebId == recipientWebId);
+ final String prevDateTime =
+ currentLogRecords[prevMatchIndex].dateTimeStr;
+ // Update record if this record more recent than stored record
+ if ([0, 1].contains(
+ DateTime.parse(record.dateTimeStr)
+ .compareTo(DateTime.parse(prevDateTime)),
+ )) {
+ currentLogRecords[prevMatchIndex] = record;
+ }
+ } else {
+ // Store record if no prev record for this recipient
+ currentLogRecords.add(record);
+ }
+ } else {
+ // Skip revoke records
+ continue;
+ }
+ }
+
+ // Refresh the UI
+ setState(() {
+ permHistoryList = currentLogRecords;
+ });
+ }
+
+ /// Private function to call alert dialog in grant permission UI context
+ Future _alert(String msg) async => alert(context, msg);
+
+ /// Build the main widget
+ Widget _buildPermPage(
+ BuildContext context, [
+ PermissionDetails? initPermDetails,
+ List? initPermHistoryList,
+ ]) {
+ // Check if future is set or not. If set display the permission map
+ if (initPermDetails != null && permTableInitialied == false) {
+ permDataMap = initPermDetails.permissionMap;
+ _ownerWebId = initPermDetails.ownerWebId;
+ _granterWebId = initPermDetails.granterWebId;
+ permDataFile = widget.resourceName!;
+ permTableInitialied = true;
+ }
+
+ if (initPermHistoryList != null && permHistoryInitialied == false) {
+ permHistoryList = initPermHistoryList;
+ // Set full unfiltered list to current list from initial
+ // log fetch
+ unFilteredPermHistoryList = initPermHistoryList;
+ permHistoryInitialied = true;
+ }
+
+ final retrievePermissionButton = ElevatedButton(
+ child: const Text('Retrieve permissions'),
+ onPressed: () async {
+ final fileName = fileNameController.text;
+ if (fileName.isEmpty) {
+ await _alert('Please enter a file name');
+ } else {
+ await _updatePermissions(fileName, isFile: isFile);
+ }
+ },
+ );
+
+ bool getIsFile() => widget.resourceName != null ? widget.isFile : isFile;
+
+ // Use customAppBar if provided
+ final customAppBar = widget.customAppBar ??
+ defaultAppBar(
+ context,
+ widget.title,
+ widget.backgroundColor,
+ widget.child,
+ onNavigateBack: () => widget.onNavigateBack?.call(),
+ getResult: () => permissionsGrantedSuccessfully,
+ );
+
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Scaffold(
+ // Display app bar if showAppBar selected
+ // AppBar will be defaultAppBar() if customAppBar()
+ // not provided
+ appBar: widget.showAppBar ? customAppBar : null,
+
+ body: Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: Column(
+ children: [
+ smallGapV,
+ // Sharing heading
+ makeHeading(
+ makeSharingTitleStr(
+ fileName: widget.resourceName,
+ isFile: widget.isFile,
+ ),
+ bold: false,
+ addColor: false,
+ addPadding: false,
+ ),
+ smallGapV,
+ // Choose resource and show _updatePermissions button
+ if (widget.resourceName == null) ...[
+ getResourceForm(
+ formController: fileNameController,
+ isFile: isFile,
+ onResourceTypeChange: (bool v) =>
+ setState(() => isFile = v),
+ ),
+ smallGapV,
+ retrievePermissionButton,
+ smallGapV,
+ ],
+ ShareResourceButton(
+ resourceName: widget.resourceName,
+ fileNameController: fileNameController,
+ accessModeList: widget.accessModeList,
+ recipientTypeList: widget.recipientTypeList,
+ updatePermissionsFunction: _updatePermissions,
+ ownerWebId: _ownerWebId,
+ granterWebId: _granterWebId,
+ isExternalRes: widget.isExternalRes,
+ isFile: widget.isFile,
+ dataFilesMap: widget.dataFilesMap,
+ onPermissionGranted: widget.onPermissionGranted,
+ ),
+
+ mediumGapV,
+ makeSubHeading(
+ showCurrentPermOnly
+ ? 'People with current access'
+ : 'Permission history',
+ addPadding: false,
+ ),
+ smallGapV,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ spacing: 5.0,
+ children: [
+ Expanded(
+ flex: 3,
+ // Search logs field
+ child: showCurrentPermOnly
+ ? const Text('')
+ : TextField(
+ onChanged: (value) => _searchLogs(value),
+ decoration: const InputDecoration(
+ labelText:
+ 'Search access level, permission type, recipient or granter name',
+ labelStyle: TextStyle(fontSize: 12),
+ hintText: 'Enter search text',
+ hintStyle: TextStyle(fontSize: 12),
+ prefixIcon: Icon(Icons.search),
+ border: OutlineInputBorder(
+ borderRadius:
+ BorderRadius.all(Radius.circular(25.0)),
+ ),
+ ),
+ ),
+ ),
+ // Current/History permission switch
+ SizedBox(
+ width: 170.0,
+ child: MarkdownTooltip(
+ message:
+ 'Switch between current people with access and permission history log',
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ spacing: 5.0,
+ children: [
+ SizedBox(
+ width: 100,
+ child: Text(
+ showCurrentPermOnly
+ ? 'Current Permissions'
+ : 'All Permissions',
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.end,
+ ),
+ ),
+ Switch(
+ // This bool value toggles the switch.
+ value: showCurrentPermOnly,
+ activeThumbColor: ActionColors.success,
+ onChanged: (bool value) {
+ // This is called when the user toggles the switch.
+ setState(() {
+ showCurrentPermOnly = value;
+ });
+ // If showing all permission
+ // default to full permission history
+ // list
+ if (!showCurrentPermOnly) {
+ setState(() {
+ permHistoryList = unFilteredPermHistoryList;
+ });
+ }
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ vSmallGapV,
+
+ // Show permissions from ACL if showCurrentPermOnly
+ // is selected, else show searchable permission
+ // history.
+
+ showCurrentPermOnly
+ ? PermissionTable(
+ resourceName: permDataFile,
+ permDataMap: permDataMap,
+ ownerWebId: _ownerWebId,
+ granterWebId: _granterWebId,
+ updatePermissionsFunction: _updatePermissions,
+ parentWidget: widget.child,
+ isFile: getIsFile(),
+ isExternalRes: widget.isExternalRes,
+ constraints: constraints,
+ )
+ : PermissionHistory(
+ // Force history rebuild on permission history change.
+
+ key: ValueKey(permHistoryList),
+ resourceName: widget.resourceName!,
+ permHistory: permHistoryList,
+ constraints: constraints,
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) => widget.resourceName == null
+ ? _buildPermPage(context)
+ : FutureBuilder(
+ future: Future.wait([
+ // Future that returns List of current access from ACL
+ getACLPerm,
+ // Future that returns List from permission log
+ getPermHistoryList,
+ ]),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Scaffold(body: loadingScreen(normalLoadingScreenHeight));
+ }
+ final PermissionDetails initCurrentPerm =
+ snapshot.data![0] as PermissionDetails;
+ final List initPermHistoryList =
+ snapshot.data![1] as List;
+ return initCurrentPerm.permissionMap.isEmpty
+ ? widget.child
+ : _buildPermPage(context, initCurrentPerm, initPermHistoryList);
+ },
+ );
+}
diff --git a/lib/src/widgets/group_webid_input.dart b/lib/src/widgets/group_webid_input.dart
new file mode 100644
index 0000000..242d891
--- /dev/null
+++ b/lib/src/widgets/group_webid_input.dart
@@ -0,0 +1,168 @@
+/// A dialog to input individual WebID.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart'
+ show checkResourceStatus, ResourceStatus, whatIsWebID, demoWebID;
+
+import 'package:solidui/solidui.dart'
+ show smallGapV, makeSubHeading, GrantPermFormLayout;
+import 'package:solidui/src/utils/solid_alert.dart';
+
+/// A [StatefulWidget] dialog for entering group of webIds.
+///
+/// Parameters:
+/// - [onSubmitFunction] - function to be called on submit.
+
+class GroupWebIdTextInput extends StatefulWidget {
+ /// Function run on Submit button press.
+ final Function onSubmitFunction;
+
+ const GroupWebIdTextInput({super.key, required this.onSubmitFunction});
+
+ @override
+ State createState() => _GroupWebIdTextInputState();
+}
+
+class _GroupWebIdTextInputState extends State {
+ /// Text controller for webId list field
+ final formControllerGroupWebIds = TextEditingController();
+
+ /// Text controller for group name for webId list
+
+ final formControllerGroupName = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ // dispose text controller when the widget is unmounted
+ @override
+ void dispose() {
+ formControllerGroupWebIds.dispose();
+ formControllerGroupName.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ smallGapV,
+ // Explain webId with example
+ MarkdownTooltip(
+ message: '$whatIsWebID Eg: $demoWebID',
+ child: makeSubHeading('Enter recipient group WebIds'),
+ ),
+ // Add padding to webid textformfield and suggestion drop down
+ Container(
+ padding: GrantPermFormLayout.inputPadding,
+ child: Column(
+ children: [
+ // Group name. Should be a single string
+ TextFormField(
+ controller: formControllerGroupName,
+ decoration: const InputDecoration(
+ labelText: 'Group name',
+ hintText:
+ 'Multiple words will be combined using the symbol -',
+ ),
+ ),
+ smallGapV,
+ // List of Web IDs divided by semicolon
+ TextFormField(
+ controller: formControllerGroupWebIds,
+ decoration: const InputDecoration(
+ labelText: 'List of WebIDs',
+ hintText: 'Divide multiple WebIDs using the semicolon (;)',
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () async {
+ // Check if all the input entries are correct
+ final groupName = formControllerGroupName.text.trim();
+ final groupWebIds = formControllerGroupWebIds.text.trim();
+
+ // Check if both fields are not empty
+ if (groupName.isNotEmpty && groupWebIds.isNotEmpty) {
+ final webIdList = groupWebIds.split(';');
+
+ // Check if all the webIds are true links
+ var trueWebIdsFlag = true;
+ for (final webId in webIdList) {
+ if (!Uri.parse(
+ webId.replaceAll('#me', ''),
+ ).isAbsolute ||
+ !(await checkResourceStatus(webId) ==
+ ResourceStatus.exist)) {
+ trueWebIdsFlag = false;
+ }
+ }
+
+ if (trueWebIdsFlag) {
+ // Save selected webid group
+ widget.onSubmitFunction(groupName, webIdList);
+ } else {
+ if (!context.mounted) return;
+ await alert(
+ context,
+ 'At least one of the Web IDs you entered is not valid',
+ );
+ }
+ } else {
+ if (!context.mounted) return;
+ await alert(
+ context,
+ 'Please enter a group name and a list of Web IDs',
+ );
+ }
+ },
+ child: const Text('Select Group of WebIds'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/widgets/ind_webid_input.dart b/lib/src/widgets/ind_webid_input.dart
new file mode 100644
index 0000000..d341622
--- /dev/null
+++ b/lib/src/widgets/ind_webid_input.dart
@@ -0,0 +1,255 @@
+/// A dialog to input Group of WebIDs.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage, Jess Moore
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart'
+ show checkResourceStatus, ResourceStatus, whatIsWebID, demoWebID;
+
+import 'package:solidui/solidui.dart'
+ show
+ smallGapV,
+ makeSubHeading,
+ GrantPermFormLayout,
+ WebIdLayout,
+ DropdownColors;
+import 'package:solidui/src/utils/solid_alert.dart';
+
+/// A [StatefulWidget] dialog for adding an individual webId.
+/// Function call requires the following inputs.
+/// [onSubmitFunction] is the function to be called on submit.
+/// [uniqRecipWebIdList] is a list of the webIds of unique recipients of the
+/// owner's data.
+///
+class IndWebIdTextInput extends StatefulWidget {
+ /// Initialise widget variables.
+
+ const IndWebIdTextInput({
+ required this.onSubmitFunction,
+ this.uniqRecipWebIdList,
+ super.key,
+ });
+
+ /// Function run on Submit button press.
+ final Function onSubmitFunction;
+
+ /// List of unique recipient webIds
+ final List? uniqRecipWebIdList;
+
+ @override
+ State createState() => _IndWebIdTextInputState();
+}
+
+class _IndWebIdTextInputState extends State {
+ /// Text controller for WebId field
+ final formControllerWebId = TextEditingController();
+
+ /// Capture whether user has started to enter text
+ bool _textEntered = false;
+
+ /// WebId list
+ List webIdList = [];
+
+ /// Initialise the matching suggestions list
+ List suggestionList = [];
+ String hint = '';
+
+ // Dispose text controller when the widget is unmounted
+ @override
+ void dispose() {
+ formControllerWebId.dispose();
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ webIdList = widget.uniqRecipWebIdList ?? [];
+ super.initState();
+ }
+
+ /// Generate advice to help user enter valid WebID
+ String? get _helpText {
+ final text = formControllerWebId.value.text.trim();
+ final uri = Uri.parse(text);
+
+ // Check for https scheme and ://
+ if (!uri.isScheme('HTTPS') || !uri.toString().contains('://')) {
+ return 'Must start with https://';
+ }
+ // Check WebID contains host followed by '/'
+
+ if (!uri.path.contains('/')) {
+ return 'Must have form https://[POD server host]/[their username]/profile/card#me';
+ }
+ // Check for WebID path with profile suffix
+ if (!uri.path.toLowerCase().contains('/profile/card')) {
+ return 'Must end with \'/[their username]/profile/card#me\'';
+ }
+ // Check ends in #me
+ if (!(uri.fragment.toLowerCase() == 'me')) {
+ return 'Must end with URL fragment #me after /profile/card';
+ }
+ // Check fully qualified web address
+ // 20250721 jm Retaining this check, may not be needed
+ if (!Uri.parse(text.replaceAll('#me', '')).isAbsolute) {
+ return 'Must be a fully qualified web address';
+ }
+ // return null if the text is valid
+ return null;
+ }
+
+ /// Generate suggestions for users based on input matches to
+ /// current complete recipient list of user
+ void filterSuggestions(String value) {
+ suggestionList.clear();
+
+ if (value.isEmpty) {
+ setState(() {});
+ return;
+ }
+ suggestionList = webIdList
+ .where((e) => e.toLowerCase().contains(value.toLowerCase()))
+ .toList();
+
+ setState(() {});
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ smallGapV,
+ // Explain webId with example
+ MarkdownTooltip(
+ message: '$whatIsWebID Eg: $demoWebID',
+ child: makeSubHeading('Enter or select recipient\'s WebId'),
+ ),
+ // Add padding to webid textformfield and suggestion drop down
+ Container(
+ padding: GrantPermFormLayout.inputPadding,
+ child: Column(
+ children: [
+ // Web ID text field
+ TextFormField(
+ controller: formControllerWebId,
+ decoration: InputDecoration(
+ labelText: 'Individual\'s webID',
+ // Once user has started entering text, use formfield
+ // error message to advise user how to specify
+ // valid webId
+ errorText: _textEntered ? _helpText : null,
+ ),
+ onFieldSubmitted: (value) {},
+ onChanged: (value) => setState(() {
+ // User has started entering text
+ _textEntered = true;
+ // Filter suggestions
+ filterSuggestions(value);
+ }),
+ ),
+ smallGapV,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ if (webIdList.isNotEmpty) ...[
+ if (suggestionList.isNotEmpty ||
+ formControllerWebId.text.isNotEmpty) ...[
+ // 20250729 jm: Wrap ListView() in fixed SizeBox() to avoid render problems in AlertDialog()
+ boxedSuggestionList(context, suggestionList),
+ ] else ...[
+ boxedSuggestionList(context, webIdList),
+ ],
+ ],
+ TextButton(
+ onPressed: () async {
+ final receiverWebId = formControllerWebId.text.trim();
+
+ // User has entered WebId text that satisfies error checks
+ if (receiverWebId.isNotEmpty && _helpText == null) {
+ // Check WebId exists
+
+ if (await checkResourceStatus(receiverWebId) ==
+ ResourceStatus.exist) {
+ // Save provided WebId
+ widget.onSubmitFunction(receiverWebId);
+ } else {
+ if (!context.mounted) return;
+ // Request WebId that exists
+ await alert(
+ context,
+ 'This WebID does not exist. Please enter the correct WebID',
+ );
+ }
+ }
+ },
+ child: const Text('Select WebId'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Flexible boxedSuggestionList(BuildContext context, List idList) {
+ return Flexible(
+ child: SizedBox(
+ height: WebIdLayout.dropdownHeight,
+ child: ListView.builder(
+ padding: WebIdLayout.listPadding,
+ itemCount: idList.length,
+ itemBuilder: (context, index) {
+ return Card(
+ elevation: WebIdLayout.dropdownElevation,
+ child: ListTile(
+ title: Text(idList[index]),
+ focusColor: DropdownColors.primary,
+ hoverColor: DropdownColors.accent,
+ splashColor: DropdownColors.primary,
+ onTap: () => setState(() {
+ // User has started entering text
+ _textEntered = true;
+ formControllerWebId.text = idList[index];
+ }),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/ind_webid_input_screen.dart b/lib/src/widgets/ind_webid_input_screen.dart
new file mode 100644
index 0000000..d623496
--- /dev/null
+++ b/lib/src/widgets/ind_webid_input_screen.dart
@@ -0,0 +1,127 @@
+/// A screen to retrieve all the webids of recipients of a user's Pod files before loading the webid input dialog.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart'
+ show getRecipientList, extractRecipWebIdList;
+
+import 'package:solidui/solidui.dart' show normalLoadingScreenHeight;
+import 'package:solidui/src/widgets/ind_webid_input.dart';
+import 'package:solidui/src/widgets/solid_loading_screen.dart';
+
+/// A screen that runs before opening the WebID input dialog, which
+/// retrieves the list of files in the owner's pod.
+///
+/// Parameters:
+/// - [onSubmitFunction] is the function to be called on submit
+///
+class IndWebIdInputScreen extends StatefulWidget {
+ /// Initialise widget variables.
+ const IndWebIdInputScreen({
+ required this.onSubmitFunction,
+ this.dataFilesMap = const {},
+ super.key,
+ });
+
+ /// Function run on Submit button press.
+ final Function onSubmitFunction;
+
+ /// Map of data files on a user's POD used to extract the
+ /// user's recipient list by the WebIdTextInputScreen.
+ /// If not provided, the file list must be read to obtain
+ /// the user's recipient list used in the WebIdTextInputScreen.
+ final Map dataFilesMap;
+
+ @override
+ State createState() => _IndWebIdInputScreenState();
+}
+
+class _IndWebIdInputScreenState extends State {
+ /// Future comprising the unique recipient WebId list of the user's Pod data.
+ static Future>? _asyncGetRecipList;
+
+ /// List of unique recipient WebId list of the user's Pod data.
+ List uniqRecipWebIdList = [];
+
+ @override
+ void initState() {
+ // Retrieve files and derive unique recipient WebId list.
+ if (widget.dataFilesMap.isEmpty) {
+ _asyncGetRecipList = getRecipientList();
+ } else {
+ // Extract unique recipient WebId list if file data provided.
+ uniqRecipWebIdList = extractRecipWebIdList(widget.dataFilesMap);
+ }
+ super.initState();
+ }
+
+ // Load Individual WebId Text Input
+ Widget _loadIndWebIdTextInput(
+ Function onSubmitFunction, [
+ List uniqRecipWebIdList = const [],
+ ]) {
+ return IndWebIdTextInput(
+ onSubmitFunction: onSubmitFunction,
+ uniqRecipWebIdList: uniqRecipWebIdList,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return (widget.dataFilesMap.isNotEmpty)
+ ? _loadIndWebIdTextInput(widget.onSubmitFunction, uniqRecipWebIdList)
+ : FutureBuilder(
+ future: _asyncGetRecipList,
+ builder: (context, snapshot) {
+ Widget returnVal;
+ if (snapshot.connectionState == ConnectionState.done) {
+ return snapshot.data == null ||
+ snapshot.data.toString() == 'null' ||
+ snapshot.data == []
+ // Load Individual WebId Input Dialog Screen without recipient list
+ ? returnVal = _loadIndWebIdTextInput(
+ widget.onSubmitFunction,
+ )
+ // Load Individual WebId Input Dialog Screen with recipient list
+ : returnVal = _loadIndWebIdTextInput(
+ widget.onSubmitFunction,
+ snapshot.data!,
+ );
+ } else {
+ returnVal = loadingScreen(normalLoadingScreenHeight);
+ }
+ return returnVal;
+ },
+ );
+ }
+}
diff --git a/lib/src/widgets/permission_checkbox.dart b/lib/src/widgets/permission_checkbox.dart
new file mode 100644
index 0000000..eb53c1b
--- /dev/null
+++ b/lib/src/widgets/permission_checkbox.dart
@@ -0,0 +1,61 @@
+/// A checkbox widget for access modes.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart' show AccessMode;
+
+/// Checkbox widget to display different access mode selections. Function call
+/// requires the following inputs
+/// [accessMode] is the AccessMode instance for the checkbox
+/// [checkboxChecked] is the boolean controller for the checkbox press
+/// [updateCheckBox] is the function to update the checkbox data when pressed
+///
+
+MarkdownTooltip permissionCheckbox(
+ AccessMode accessMode,
+ bool checkboxChecked,
+ Function updateCheckBox,
+) {
+ return MarkdownTooltip(
+ message: accessMode.description,
+ child: CheckboxListTile(
+ title: Text(accessMode.mode),
+ value: checkboxChecked,
+ onChanged: (newValue) {
+ updateCheckBox(newValue, accessMode);
+ },
+ controlAffinity: ListTileControlAffinity.leading, // <-- leading Checkbox
+ ),
+ );
+}
diff --git a/lib/src/widgets/permission_history.dart b/lib/src/widgets/permission_history.dart
new file mode 100644
index 0000000..dadebac
--- /dev/null
+++ b/lib/src/widgets/permission_history.dart
@@ -0,0 +1,183 @@
+/// Table listing permission history of a resource.
+///
+// Time-stamp:
+///
+/// Copyright (C) 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: Jess Moore
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart' show LogRecord;
+
+import 'package:solidui/src/constants/ui_common.dart'
+ show ListIconSize, listIconShape;
+import 'package:solidui/src/constants/ui_window.dart'
+ show WindowSize, ListItemSize;
+
+/// A [StatefulWidget] for listing the permission history of a resource.
+///
+/// Parameters:
+/// - [resourceName] - The filename or file url of the resource.
+/// - [permHistory] is the [List] comprising permission history for
+/// the [resourceName].
+
+class PermissionHistory extends StatefulWidget {
+ /// The name of the file or directory for which permissions are being
+ /// shown.
+
+ final String resourceName;
+
+ /// Map of access permission data being displayed for [resourceName].
+
+ final List permHistory;
+
+ /// Layout constraints
+
+ final BoxConstraints constraints;
+
+ const PermissionHistory({
+ super.key,
+ required this.resourceName,
+ required this.permHistory,
+ required this.constraints,
+ });
+
+ @override
+ State createState() => _PermissionHistoryState();
+}
+
+class _PermissionHistoryState extends State {
+ /// Searched/sorted logs
+ List _permHistory = [];
+
+ /// Aspect ratio (width / height) for gridview
+ /// cards to display log items
+ late double cardAspectRatio = 2.0;
+
+ /// Boolean describing whether window is narrow
+ late bool isNarrow;
+
+ /// Scroll controller for single child scroll view
+ late final ScrollController _scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // By default _permissions is the full list of permissions
+ _permHistory = widget.permHistory;
+
+ // Create scroll controller
+ _scrollController = ScrollController();
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose(); // Dispose the ScrollController
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Derive whether window is narrow
+ isNarrow = WindowSize().isNarrowWindow(widget.constraints);
+ // Calculate the aspect radio for grid cards
+ cardAspectRatio =
+ ListItemSize().calculateCardAspectRatio(widget.constraints);
+
+ return Expanded(
+ child: GridView.builder(
+ controller: _scrollController,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ // Aspect ratio calculated from LayoutBuilder box constraints
+ crossAxisCount: 1,
+ childAspectRatio: cardAspectRatio,
+ ),
+ padding: const EdgeInsets.all(10),
+ itemCount: _permHistory.length, //widget.permDataMap.length,
+ itemBuilder: (context, index) => Card(
+ child: Center(
+ child: Container(
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(5)),
+ ),
+ child: MarkdownTooltip(
+ message: _permHistory[index].toolTip,
+ child: ListTile(
+ // Leading icon denoting agreement of access terms
+ leading: SizedBox(
+ width: ListIconSize.width,
+ child: Center(
+ child: Ink(
+ padding: const EdgeInsets.all(8),
+ decoration: listIconShape,
+ child: Icon(
+ _permHistory[index].permissionType == 'grant'
+ ? Icons.person_add
+ : Icons.person_remove,
+ ),
+ // 20260201 jesscmoore Alternatives tried:
+ // insert_drive_file, list, lock and lock_open,
+ // mode_edit,my_library_books, note, notes, person_add,
+ // person_remove, playlist_add, playlist_remove, public,
+ // public_off, post_add, receipt, receipt_long,
+ // recent_actors,
+ ),
+ ),
+ ),
+ // Permission item title
+
+ title: Text(
+ _permHistory[index].permissionType == 'grant'
+ ? '${_permHistory[index].dateTime}: '
+ '${_permHistory[index].recipientName} '
+ '${_permHistory[index].permissionTypeLabel} '
+ '${_permHistory[index].permissionList} '
+ 'access'
+ : '${_permHistory[index].dateTime}: '
+ '${_permHistory[index].permissionList} '
+ 'access ${_permHistory[index].permissionTypeLabel} to '
+ '${_permHistory[index].recipientName}',
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ subtitle: Text(
+ 'Granter: ${_permHistory[index].granterName}',
+ maxLines: 3, // Limit lines
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/permission_table.dart b/lib/src/widgets/permission_table.dart
new file mode 100644
index 0000000..b41a493
--- /dev/null
+++ b/lib/src/widgets/permission_table.dart
@@ -0,0 +1,222 @@
+/// Table listing permissions of a resource.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore, Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart' show Permission, permMapToList;
+
+import 'package:solidui/solidui.dart'
+ show WindowSize, ListItemSize, ListIconSize, listIconShape;
+import 'package:solidui/src/widgets/revoke_permission_button.dart';
+
+/// A [StatefulWidget] for listing the permissions of a resource.
+///
+/// Parameters:
+/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource.
+/// - [permDataMap] is the map of permission data for the [resourceName]
+/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned.
+/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned.
+/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory.
+/// - [isExternalRes] - Boolean flag describing whether the resource
+/// is externally owned.
+/// - [updatePermissionsFunction] is the function to be called to refresh the permission table.
+/// - [parentWidget] is the widget to return to after an action Eg: deletion of a
+/// permission
+///
+
+class PermissionTable extends StatefulWidget {
+ /// The name of the file or directory for which permissions are being
+ /// shown.
+
+ final String resourceName;
+
+ /// Map of access permission data being displayed for [resourceName].
+
+ final Map permDataMap;
+
+ /// WebId of the resource owner.
+
+ final String ownerWebId;
+
+ /// WebId of the user granting/revoking access to the resource.
+
+ final String granterWebId;
+
+ /// A flag denoting whether resource is externally owned.
+
+ final bool isExternalRes;
+
+ /// A flag to determine whether the given resource is a file or not.
+
+ final bool isFile;
+
+ /// Function run to update permissions table
+
+ final Function updatePermissionsFunction;
+
+ /// Parent widget to return to.
+
+ final Widget parentWidget;
+
+ /// Layout constraints
+
+ final BoxConstraints constraints;
+
+ const PermissionTable({
+ super.key,
+ required this.resourceName,
+ required this.permDataMap,
+ required this.ownerWebId,
+ required this.granterWebId,
+ required this.updatePermissionsFunction,
+ required this.parentWidget,
+ required this.isFile,
+ this.isExternalRes = false,
+ required this.constraints,
+ });
+
+ @override
+ State createState() => _PermissionTableState();
+}
+
+class _PermissionTableState extends State {
+ /// Searched/sorted notes
+ List _permissions = [];
+
+ /// Aspect ratio (width / height) for gridview
+ /// cards to display note items
+ late double cardAspectRatio = 2.0;
+
+ /// Boolean describing whether window is narrow
+ late bool isNarrow;
+
+ /// Scroll controller for single child scroll view
+ late final ScrollController _scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Create scroll controller
+ _scrollController = ScrollController();
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose(); // Dispose the ScrollController
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Derive whether window is narrow
+ isNarrow = WindowSize().isNarrowWindow(widget.constraints);
+ // Calculate the aspect radio for grid cards
+ cardAspectRatio = ListItemSize().calculateCardAspectRatio(
+ widget.constraints,
+ );
+
+ // By default _permissions is the full list of permissions
+ _permissions = permMapToList(widget.permDataMap);
+
+ return Expanded(
+ child: GridView.builder(
+ controller: _scrollController,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ // Aspect ratio calculated from LayoutBuilder box constraints
+ crossAxisCount: 1,
+ childAspectRatio: cardAspectRatio,
+ ),
+ padding: const EdgeInsets.all(10),
+ itemCount: _permissions.length, //widget.permDataMap.length,
+ itemBuilder: (context, index) => Card(
+ child: Center(
+ child: Container(
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(5)),
+ ),
+ child: MarkdownTooltip(
+ message: _permissions[index].toolTip,
+ child: ListTile(
+ // Leading icon denoting agreement of access terms
+ leading: SizedBox(
+ width: ListIconSize.width,
+ child: Center(
+ child: Ink(
+ padding: const EdgeInsets.all(8),
+ decoration: listIconShape,
+ child: const Icon(Icons.handshake),
+ ),
+ ),
+ ),
+ // Permission item title
+ title: Text(
+ _permissions[index].recipientName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ subtitle: Text(
+ 'Recipient type: ${_permissions[index].recipientType} \n'
+ 'WebId: ${_permissions[index].recipientWebId} \n'
+ 'Permissions: ${_permissions[index].permList.join(', ')}',
+ maxLines: 4, // Limit to 4 lines
+ overflow: TextOverflow.ellipsis,
+ ),
+ // Show revoke button for recipientWebId != ownerWebId
+ trailing: (widget.ownerWebId !=
+ _permissions[index].recipientWebId)
+ ? SizedBox(
+ height: ListIconSize.height,
+ width: ListIconSize.twoIconWidth,
+ child: RevokePermissionButton(
+ resourceName: widget.resourceName,
+ permDataMap: widget.permDataMap,
+ receiverWebId: _permissions[index].recipientWebId,
+ ownerWebId: widget.ownerWebId,
+ granterWebId: widget.granterWebId,
+ isFile: widget.isFile,
+ isExternalRes: widget.isExternalRes,
+ updatePermissionsFunction:
+ widget.updatePermissionsFunction,
+ ),
+ )
+ : null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/revoke_permission_button.dart b/lib/src/widgets/revoke_permission_button.dart
new file mode 100644
index 0000000..68e76de
--- /dev/null
+++ b/lib/src/widgets/revoke_permission_button.dart
@@ -0,0 +1,184 @@
+/// A button for revoking permission.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore, Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/solidui.dart' show ActionColors;
+import 'package:solidui/src/utils/snack_bar.dart';
+
+/// A [StatefulWidget] for the revoke permission icon button. Updates
+/// owner's ACL for resource, updates owner, granter, recipient logs,
+/// and calls updatePermissions() to refresh permission table data.
+///
+/// Parameters:
+/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource.
+/// - [permDataMap] is the map of permission data for the [resourceName]
+/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned.
+/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned.
+/// - [receiverWebId] - WebId with access to the resource, one of ownerWebId, granterWebId or recipientWebId.
+/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory.
+/// - [isExternalRes] - Boolean flag describing whether the resource
+/// is externally owned.
+/// - [updatePermissionsFunction] is the function to be called to refresh the permission table.
+///
+
+class RevokePermissionButton extends StatefulWidget {
+ /// The name of the file or directory for which permissions are being
+ /// shown.
+
+ final String resourceName;
+
+ /// Map of access permission data being displayed for [resourceName].
+
+ final Map permDataMap;
+
+ /// WebId with access to resource.
+
+ final String receiverWebId;
+
+ /// WebId of the resource owner.
+
+ final String ownerWebId;
+
+ /// WebId of the user granting/revoking access to the resource.
+
+ final String granterWebId;
+
+ /// A flag denoting whether resource is externally owned.
+
+ final bool isExternalRes;
+
+ /// A flag to determine whether the given resource is a file or not.
+
+ final bool isFile;
+
+ /// Function run to update permissions table
+
+ final Function updatePermissionsFunction;
+
+ const RevokePermissionButton({
+ super.key,
+ required this.resourceName,
+ required this.permDataMap,
+ required this.receiverWebId,
+ required this.ownerWebId,
+ required this.granterWebId,
+ required this.updatePermissionsFunction,
+ required this.isFile,
+ this.isExternalRes = false,
+ });
+
+ @override
+ State createState() => _RevokePermissionButtonState();
+}
+
+class _RevokePermissionButtonState extends State {
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MarkdownTooltip(
+ message: 'Revoke all access to this recipient',
+ child: IconButton(
+ icon: const Icon(
+ Icons.delete,
+ size: 24.0,
+ color: ActionColors.delete,
+ ),
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (ctx) {
+ return AlertDialog(
+ title: const Text('Please Confirm'),
+ content: Text(
+ 'Are you sure you want to remove the [${(widget.permDataMap[widget.receiverWebId][permStr] as List).join(', ')}] permission/s from ${widget.receiverWebId.replaceAll('.ttl', '')}?',
+ ),
+ actions: [
+ // The "Yes" button
+ TextButton(
+ onPressed: () async {
+ await revokePermission(
+ fileName: widget.resourceName,
+ isFile: widget.isFile,
+ permissionList: widget.permDataMap[widget.receiverWebId]
+ [permStr] as List,
+ recipientIndOrGroupWebId: widget.receiverWebId,
+ ownerWebId: widget.ownerWebId,
+ granterWebId: widget.granterWebId,
+ recipientType: getRecipientType(
+ widget.permDataMap[widget.receiverWebId][agentStr]
+ as String,
+ widget.receiverWebId,
+ ),
+ isExternalRes: widget.isExternalRes,
+ );
+
+ if (ctx.mounted) {
+ Navigator.pop(ctx);
+ }
+ if (ctx.mounted) {
+ showSnackBar(
+ context,
+ 'Permission revoked successfully!',
+ ActionColors.success,
+ );
+ }
+ await widget.updatePermissionsFunction(
+ widget.resourceName,
+ isFile: widget.isFile,
+ );
+ },
+ child: const Text('Yes'),
+ ),
+ TextButton(
+ onPressed: () {
+ // Close the dialog
+ Navigator.of(ctx).pop();
+ },
+ child: const Text('No'),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/select_recipients.dart b/lib/src/widgets/select_recipients.dart
new file mode 100644
index 0000000..05a8ba2
--- /dev/null
+++ b/lib/src/widgets/select_recipients.dart
@@ -0,0 +1,182 @@
+/// Button list for selecting recipients.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore, Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart' show RecipientType;
+
+import 'package:solidui/solidui.dart'
+ show granterRecipientTypes, ownerRecipientTypes, recipientToolTips;
+
+/// A [StatefulWidget] for a container of buttons for
+/// selecting recipients in the grant permission form.
+///
+/// Parameters:
+/// - [isExternalRes] - Boolean flag describing whether
+/// the resource is externally owned.
+/// - [recipientTypeList] - List of recipient type options to show.
+/// - [setPublicFunction] - a function for setting recipients to
+/// the public.
+/// - [setAuthUsersFunction] - a function for setting recipients
+/// to all authorised users.
+/// - [setIndividualFunction] - a function for selecting an
+/// individual recipient webId.
+/// - [setGroupFunction] - a function for selecting a
+/// group of webIds as recipients.
+
+class SelectRecipients extends StatefulWidget {
+ /// A flag denoting whether the resource is externally owned.
+
+ final bool isExternalRes;
+
+ /// The list of types of recipients to show in form. By default
+ /// all four types of recipient are listed.
+
+ final List recipientTypeList;
+
+ /// A function for setting recipients to the public.
+
+ final Function setPublicFunction;
+
+ /// A function for setting recipients to all authorised users.
+
+ final Function setAuthUsersFunction;
+
+ /// A function for selecting an individual recipient webId.
+
+ final Function setIndividualFunction;
+
+ /// A function for selecting a group of webIds as recipients.
+
+ final Function setGroupFunction;
+
+ const SelectRecipients({
+ super.key,
+ required this.isExternalRes,
+ required this.recipientTypeList,
+ required this.setPublicFunction,
+ required this.setAuthUsersFunction,
+ required this.setIndividualFunction,
+ required this.setGroupFunction,
+ });
+
+ @override
+ State createState() => _SelectRecipientsState();
+}
+
+class _SelectRecipientsState extends State {
+ /// Define recipient type list
+
+ List recipientTypeList = [];
+
+ /// Selected recipient type
+
+ RecipientType? _selectedRecipientType;
+
+ // Allowed recipient types
+
+ List allowedRecipientTypes = [];
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Load recipient type list to be displayed
+ for (final recTypeStr in widget.recipientTypeList) {
+ recipientTypeList.add(RecipientType.getInstanceByValue(recTypeStr));
+ }
+
+ // jesscmoore 20260118: requires check grant/revoke to
+ // public/auth works on external resources
+ // av 20250526:
+ // Public and Authenticated recipient buttons are
+ // disabled currently because
+ // providing public or authenticated permissions to
+ // external resources is not yet implemented in
+ // [grantPermission()] function.
+ widget.isExternalRes
+ ? allowedRecipientTypes = granterRecipientTypes
+ : allowedRecipientTypes = ownerRecipientTypes;
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ children: [
+ RadioGroup(
+ groupValue: _selectedRecipientType,
+ onChanged: (RecipientType? value) {
+ setState(() {
+ _selectedRecipientType = value;
+ });
+ switch (value) {
+ case RecipientType.public:
+ widget.setPublicFunction();
+ case RecipientType.authUser:
+ widget.setAuthUsersFunction();
+ case RecipientType.individual:
+ widget.setIndividualFunction();
+ case RecipientType.group:
+ widget.setGroupFunction();
+ case RecipientType.none:
+ return;
+ case null:
+ return;
+ }
+ },
+ child: Column(
+ children: [
+ for (final rtype in ownerRecipientTypes)
+ if (recipientTypeList.contains(rtype))
+ MarkdownTooltip(
+ message: recipientToolTips[rtype]!,
+ child: ListTile(
+ title: Text(rtype.description),
+ leading: Radio(value: rtype),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/share_resource_button.dart b/lib/src/widgets/share_resource_button.dart
new file mode 100644
index 0000000..cff4870
--- /dev/null
+++ b/lib/src/widgets/share_resource_button.dart
@@ -0,0 +1,217 @@
+/// A button for sharing a resource.
+///
+// Time-stamp:
+///
+/// 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: Jess Moore
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidui/solidui.dart' show SharingPageLayout;
+import 'package:solidui/src/utils/solid_alert.dart';
+import 'package:solidui/src/widgets/grant_permission_form.dart';
+
+/// A [StatefulWidget] for sharing a resource, by either creating
+/// an access permission for a new recipient or updating the access
+/// permission of an existing recipient.
+///
+/// Parameters:
+/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource.
+/// - [fileNameController] - The [TextEditingController] for the filename
+/// field.
+/// - [isExternalRes] - Boolean flag describing whether the resource
+/// is externally owned.
+/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned.
+/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned.
+/// - [accessModeList] - List of access mode options to show.
+/// - [recipientTypeList] - List of recipient type options to show.
+/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory.
+/// - [updatePermissionsFunction] is the function to be called to refresh the permission table.
+///
+/// - [onPermissionGranted] - Callback function called when permissions are granted successfully.
+
+class ShareResourceButton extends StatefulWidget {
+ final TextEditingController fileNameController;
+
+ /// String to assign the webId of the resource owner.
+
+ final String ownerWebId;
+
+ /// String to assign the external webId of the resource granter.
+
+ final String granterWebId;
+
+ /// The name of the file or directory that access is being granted for.
+
+ final String? resourceName;
+
+ final bool isExternalRes;
+
+ /// A flag to determine whether the given resource is a file or not.
+
+ final bool isFile;
+
+ /// The list of access modes to be displayed. By default all four types of
+ /// access mode are listed.
+
+ final List accessModeList;
+
+ /// The list of types of recipients receiving permission to access the resource. By default all four
+ /// types of recipient are listed.
+
+ final List recipientTypeList;
+
+ /// Map of data files on a user's POD used to extract the
+ /// user's recipient list by the WebIdTextInputScreen.
+ /// If not provided, the WebIdTextInputScreen will read the
+ /// user's files in their app data folder on their Pod to
+ /// fetch the ACLs needed to derive the user's recipient list.
+
+ final Map dataFilesMap;
+
+ /// Function run to update permissions table
+
+ final Function updatePermissionsFunction;
+
+ /// Callback function called when permissions are granted successfully.
+
+ final VoidCallback? onPermissionGranted;
+
+ const ShareResourceButton({
+ super.key,
+ required this.fileNameController,
+ required this.updatePermissionsFunction,
+ this.resourceName,
+ required this.ownerWebId,
+ required this.granterWebId,
+ this.accessModeList = const ['read', 'write', 'append', 'control'],
+ this.recipientTypeList = const ['public', 'indi', 'auth', 'group'],
+ required this.isExternalRes,
+ required this.isFile,
+ this.dataFilesMap = const {},
+ this.onPermissionGranted,
+ });
+
+ @override
+ State createState() => _ShareResourceButtonState();
+}
+
+class _ShareResourceButtonState extends State {
+ /// Filename text controller
+
+ late final TextEditingController _fileNameController;
+
+ /// Owner WebId
+
+ late final String _ownerWebId;
+
+ /// Granter WebId
+
+ late final String _granterWebId;
+
+ /// Selected resource - assigned on Share Resource button press
+
+ String _resourceName = '';
+
+ /// A flag to identify if the resource is a file or not
+
+ bool isFile = true;
+
+ /// Flag to track if permissions were granted successfully.
+
+ bool permissionsGrantedSuccessfully = false;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _fileNameController = widget.fileNameController;
+ _ownerWebId = widget.ownerWebId;
+ _granterWebId = widget.granterWebId;
+ }
+
+ @override
+ void dispose() {
+ _fileNameController.dispose(); // Dispose filename editing controller
+ super.dispose();
+ }
+
+ /// Mark permissions as granted successfully for callback tracking
+ Future _updatePermissionGrantedStatus() async {
+ setState(() => permissionsGrantedSuccessfully = true);
+ }
+
+ /// Private function to call alert dialog in share resource button
+ /// context. This provides an alert dialog over the top of the
+ /// grant permission form dialog.
+ Future _alert(String msg) async => alert(context, msg);
+
+ // Resource is a file if resource selected in GrantPermissionUi()
+ bool _getIsFile() => widget.resourceName != null ? widget.isFile : isFile;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: SharingPageLayout.inputPadding,
+ child: ElevatedButton.icon(
+ icon: const Icon(Icons.share),
+ onPressed: () async {
+ // Assign dataFile if null (first Grant press)
+ _resourceName = widget.resourceName ?? _fileNameController.text;
+
+ if (_resourceName != '') {
+ // Display GrantPermissionForm dialog to enter
+ // recipient and access modes
+ await showDialog(
+ context: context,
+ builder: (BuildContext dialogContext) {
+ return GrantPermissionForm(
+ resourceName: _resourceName,
+ accessModeList: widget.accessModeList,
+ recipientTypeList: widget.recipientTypeList,
+ updatePermissionsFunction: widget.updatePermissionsFunction,
+ ownerWebId: _ownerWebId,
+ granterWebId: _granterWebId,
+ isExternalRes: widget.isExternalRes,
+ isFile: _getIsFile(),
+ dataFilesMap: widget.dataFilesMap,
+ updatePermissionGrantedFunction:
+ _updatePermissionGrantedStatus,
+ onPermissionGranted: widget.onPermissionGranted,
+ );
+ },
+ );
+ } else {
+ await _alert('Please select one or more recipients');
+ }
+ },
+ label: const Text('Share Resource'),
+ ),
+ );
+ }
+}
diff --git a/lib/src/widgets/shared_resources_table.dart b/lib/src/widgets/shared_resources_table.dart
new file mode 100644
index 0000000..1c6d3d7
--- /dev/null
+++ b/lib/src/widgets/shared_resources_table.dart
@@ -0,0 +1,231 @@
+/// A table displaying permission data for a given file.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/src/utils/solid_alert.dart';
+import 'package:solidui/src/widgets/file_explorer.dart';
+
+/// Build the permission table widget. Function call requires the
+/// following inputs
+/// [context] is the BuildContext from which this function is called.
+/// [sharedResMap] is the map containing data of shared resources.
+/// [parentWidget] is the widget to return to after an action Eg: deletion of a
+/// permission
+
+Widget buildSharedResourcesTable(
+ BuildContext context,
+ Map sharedResMap,
+ Widget parentWidget,
+) {
+ final cWidth = MediaQuery.of(context).size.width * 0.18;
+ DataColumn buildDataColumn(String title, String tooltip) {
+ return DataColumn(
+ label: Expanded(child: Center(child: Text(title))),
+ tooltip: tooltip,
+ );
+ }
+
+ DataCell buildDataCell(String content) {
+ return DataCell(
+ SizedBox(
+ width: cWidth,
+ child: Column(children: [SelectableText(content)]),
+ ),
+ );
+ }
+
+ return Row(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: DataTable(
+ dataRowMaxHeight: double.infinity,
+ horizontalMargin: 10,
+ columnSpacing: 10,
+ columns: [
+ buildDataColumn(
+ 'Resource URL',
+ 'WebID of the POD receiving permissions',
+ ),
+ buildDataColumn('Shared on', 'Shared date and time'),
+ buildDataColumn('Owner', 'Resource owner WebID'),
+ buildDataColumn('Granter', 'Permission granter WebID'),
+ buildDataColumn('Permissions', 'List of permissions given'),
+ buildDataColumn('View/Open', 'View file'),
+ ],
+ rows: sharedResMap.keys.map((index) {
+ return DataRow(
+ cells: [
+ DataCell(
+ Container(
+ padding: const EdgeInsets.fromLTRB(0, 5, 0, 0),
+ width: cWidth,
+ child: SelectableText(
+ index as String,
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ),
+ DataCell(
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ getDateTime(
+ sharedResMap[index][PermissionLogLiteral.logtime]
+ as String,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ buildDataCell(
+ sharedResMap[index][PermissionLogLiteral.owner] as String,
+ ),
+ buildDataCell(
+ sharedResMap[index][PermissionLogLiteral.granter] as String,
+ ),
+ buildDataCell(
+ sharedResMap[index][PermissionLogLiteral.permissions]
+ as String,
+ ),
+ DataCell(
+ isDir(index)
+ ? IconButton(
+ icon: const Icon(
+ Icons.folder_open_outlined,
+ size: 24.0,
+ color: Colors.blueAccent,
+ ),
+ onPressed: () async {
+ if (!sharedResMap[index]
+ [PermissionLogLiteral.permissions]
+ .contains('read')) {
+ await alert(
+ context,
+ 'You do not have read permission to this resource!',
+ );
+ } else {
+ bool isEditable = [
+ AccessMode.write.mode,
+ AccessMode.control.mode,
+ ].any(
+ (mode) => sharedResMap[index]
+ [PermissionLogLiteral.permissions]
+ .contains(mode.toLowerCase()),
+ );
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => FileExplorerScreen(
+ folderPath: index,
+ isEditable: isEditable,
+ ownerWebId: sharedResMap[index]
+ [PermissionLogLiteral.owner]
+ as String,
+ child: parentWidget,
+ ),
+ ),
+ );
+ }
+ },
+ )
+ : IconButton(
+ icon: const Icon(
+ Icons.visibility,
+ size: 24.0,
+ color: Colors.blueAccent,
+ ),
+ onPressed: () async {
+ try {
+ // Get file content
+ final fileContent = await readExternalPod(
+ index,
+ );
+
+ if (!context.mounted) return;
+ await showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: const Text('File content'),
+ content: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ width: double.infinity,
+ height: 300,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(
+ 15,
+ ),
+ ),
+ child: Text(fileContent),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ // Close the dialog
+ Navigator.of(ctx).pop();
+ },
+ child: const Text('Ok'),
+ ),
+ ],
+ ),
+ );
+ } on Object catch (e, trace) {
+ debugPrint(e.toString());
+ debugPrint(trace.toString());
+ if (!context.mounted) return;
+ await alert(
+ context,
+ 'The file $index could not be found!',
+ );
+ }
+ },
+ ),
+ ),
+ ],
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ ],
+ );
+}
diff --git a/lib/src/widgets/shared_resources_ui.dart b/lib/src/widgets/shared_resources_ui.dart
new file mode 100644
index 0000000..d935b41
--- /dev/null
+++ b/lib/src/widgets/shared_resources_ui.dart
@@ -0,0 +1,192 @@
+/// A screen to demonstrate the data sharing capabilities of PODs.
+///
+// Time-stamp:
+///
+/// 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: Anushka Vidanage
+
+library;
+
+import 'package:flutter/material.dart';
+
+import 'package:solidpod/solidpod.dart';
+
+import 'package:solidui/solidui.dart'
+ show
+ normalLoadingScreenHeight,
+ smallGapV,
+ largeGapV,
+ makeHeading,
+ makeSubHeading;
+import 'package:solidui/src/widgets/app_bar.dart';
+import 'package:solidui/src/widgets/shared_resources_table.dart';
+import 'package:solidui/src/widgets/solid_loading_screen.dart';
+
+/// A widget for the demonstration screen of the application.
+
+class SharedResourcesUi extends StatefulWidget {
+ /// Initialise widget variables.
+
+ const SharedResourcesUi({
+ required this.child,
+ this.title = 'Demonstrating retrieve shared data functionality',
+ this.backgroundColor = const Color.fromARGB(255, 210, 210, 210),
+ this.showAppBar = true,
+ this.sourceWebId,
+ this.fileName,
+ this.customAppBar,
+ super.key,
+ });
+
+ /// The child widget to return to when back button is pressed.
+ final Widget child;
+
+ /// The text appearing in the app bar.
+ final String title;
+
+ /// The text appearing in the app bar.
+ final Color backgroundColor;
+
+ /// The boolean to decide whether to display an app bar or not
+ final bool showAppBar;
+
+ /// The webId of the owner of a resource. This is a non required
+ /// parameter. If not set UI will display all resources
+ final String? sourceWebId;
+
+ /// The name of the resource being shared. This is a non required
+ /// parameter. If not set UI will display all resources
+ final String? fileName;
+
+ /// App specific app bar. If not set default app bar will be displayed.
+ final PreferredSizeWidget? customAppBar;
+
+ @override
+ SharedResourcesUiState createState() => SharedResourcesUiState();
+}
+
+/// Class to build a UI for granting permission to a given file
+class SharedResourcesUiState extends State
+ with SingleTickerProviderStateMixin {
+ /// Permission data map of a file
+ Map permDataMap = {};
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ /// Build the main widget
+ Widget _buildSharedResourcePage(
+ BuildContext context,
+ List