diff --git a/dart_dependency_validator.yaml b/dart_dependency_validator.yaml index db9a30b8..ae00a763 100644 --- a/dart_dependency_validator.yaml +++ b/dart_dependency_validator.yaml @@ -1,2 +1,5 @@ +exclude: + - "example/**" + ignore: - cupertino_icons # An asset repository. diff --git a/example/lib/dialogs/file_metadata.dart b/example/lib/dialogs/file_metadata.dart new file mode 100644 index 00000000..cb3dfd7e --- /dev/null +++ b/example/lib/dialogs/file_metadata.dart @@ -0,0 +1,80 @@ +/// A dialog to display file metadata information. +/// +/// Copyright (C) 2024, Software Innovation Institute, ANU. +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"). +/// +/// License: https://opensource.org/license/gpl-3-0. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . +/// +/// Authors: Dawei Chen + +library; + +import 'package:flutter/material.dart'; + +/// Shows a dialog displaying file metadata. + +void showFileMetadataDialog({ + required BuildContext context, + required String fileName, + required String contentLength, + required String lastModified, + required String contentType, + required String allowdAccess, +}) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('File Information'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _infoRow('File name', fileName), + _infoRow('Last modified', lastModified), + _infoRow('Content length', contentLength), + _infoRow('Content type', contentType), + _infoRow('Allowed operations', allowdAccess), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); +} + +Widget _infoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded(child: Text(value)), + ], + ), + ); +} diff --git a/example/lib/features/create_acl_inherited_file.dart b/example/lib/features/create_acl_inherited_file.dart index 61c996ac..24d687cd 100644 --- a/example/lib/features/create_acl_inherited_file.dart +++ b/example/lib/features/create_acl_inherited_file.dart @@ -25,10 +25,10 @@ library; import 'package:flutter/material.dart'; -import 'package:demopod/constants/app.dart'; - import 'package:solidpod/solidpod.dart' show writePod, setInheritKeyDir; +import 'package:demopod/constants/app.dart'; + // A widget to create a resource with inherited ACL. // // The resource will be created inside a parent directory and the ACL of that diff --git a/example/lib/features/edit_keyvalue.dart b/example/lib/features/edit_keyvalue.dart index ba406fa0..02bdb119 100644 --- a/example/lib/features/edit_keyvalue.dart +++ b/example/lib/features/edit_keyvalue.dart @@ -27,13 +27,13 @@ library; import 'package:flutter/material.dart'; -import 'package:demopod/constants/app.dart'; -import 'package:demopod/dialogs/alert.dart'; -import 'package:demopod/utils/rdf.dart'; import 'package:editable/editable.dart'; +import 'package:solidpod/solidpod.dart' show isUserLoggedIn, writePod; import 'package:solidui/solidui.dart' show getKeyFromUserIfRequired; -import 'package:solidpod/solidpod.dart' show isUserLoggedIn, writePod; +import 'package:demopod/constants/app.dart'; +import 'package:demopod/dialogs/alert.dart'; +import 'package:demopod/utils/rdf.dart'; class KeyValueEdit extends StatefulWidget { /// Constructor diff --git a/example/lib/features/file_service.dart b/example/lib/features/file_service.dart index 8147eb27..1b3143cd 100644 --- a/example/lib/features/file_service.dart +++ b/example/lib/features/file_service.dart @@ -25,11 +25,12 @@ library; import 'package:flutter/material.dart'; -import 'package:demopod/dialogs/alert.dart'; import 'package:file_picker/file_picker.dart'; - import 'package:solidpod/solidpod.dart'; +import 'package:demopod/dialogs/alert.dart'; +import 'package:demopod/widgets/file_service_sections.dart'; + class FileService extends StatefulWidget { const FileService({required this.child, required this.webId, super.key}); final String webId; @@ -76,37 +77,6 @@ class _FileServiceState extends State { return folder.isNotEmpty ? folder : null; } - Widget getProgressBar(String message, bool isDone, double percent) { - const textStyle = TextStyle( - color: Colors.green, - fontWeight: FontWeight.bold, - ); - - final prefix = Text(message, style: textStyle); - final suffix = Text('${(percent * 100).toInt()}%', style: textStyle); - final progress = SizedBox( - width: 300, - height: 10, - child: LinearProgressIndicator( - value: percent, - minHeight: 2, - backgroundColor: Colors.black12, - color: Colors.greenAccent, - ), - ); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - prefix, - smallGapH, - progress, - smallGapH, - suffix, - ], - ); - } - @override void initState() { super.initState(); @@ -363,199 +333,44 @@ class _FileServiceState extends State { // Widgets of the file upload section - final uploadSection = [ - Text( - 'Upload a local large file and save it as "$defaultRemoteFileName" in POD', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - smallGapV, - Table( - columnWidths: const { - 0: FixedColumnWidth(450), - // 1: FixedColumnWidth(50), - // 1: FlexColumnWidth(), - }, - children: [ - TableRow( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - uploadFile ?? - 'Click the Browse button to choose a local file', - style: TextStyle( - color: uploadFile == null ? Colors.red : Colors.blue, - fontStyle: FontStyle.italic, - fontSize: 16, - ), - ), - smallGapH, - if (uploadDone) const Icon(Icons.done, color: Colors.green), - ], - ), - ], - ), - TableRow( - children: [ - TextFormField( - controller: remoteFolderController, - enabled: !(uploadInProgress || uploadDone), - decoration: const InputDecoration( - // labelText: 'Remote Folder', - // border: OutlineInputBorder(), - hintText: '(Optional) save to folder in POD, e.g. dir1/dir2/', - hintStyle: TextStyle( - color: Colors.brown, - fontStyle: FontStyle.italic, - fontSize: 15, - ), - ), - // validator: (value) { - // if (value != null || value!.trim().isNotEmpty) { - // if (!value.endsWith('/')) { - // return 'Folder path must ends with /'; - // } - // } - // return null; - // }, - ), - ], - ), - TableRow(children: [ - TextFormField( - controller: keyRefFolderController, - enabled: !(uploadInProgress || uploadDone), - decoration: const InputDecoration( - hintText: - '(Optional) Inherit encryption key of folder in POD, e.g. dir1/', - hintStyle: TextStyle( - color: Colors.brown, - fontStyle: FontStyle.italic, - fontSize: 15, - ), - ), - ), - ]), - ], - ), - smallGapV, - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - browseButton, - smallGapH, - uploadButton, - ], - ), - ]; + final uploadSection = buildUploadSectionUI( + defaultRemoteFileName: defaultRemoteFileName, + uploadFile: uploadFile, + uploadDone: uploadDone, + uploadInProgress: uploadInProgress, + remoteFolderController: remoteFolderController, + keyRefFolderController: keyRefFolderController, + browseButton: browseButton, + uploadButton: uploadButton, + ); // Widgets of the file download section - final downloadSection = [ - Text( - 'Download the "$defaultRemoteFileName" from POD', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - smallGapV, - if (downloadFile != null) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Save file'), - smallGapH, - Text( - downloadFile!, - style: const TextStyle(color: Colors.blue), - ), - smallGapH, - if (downloadDone) const Icon(Icons.done, color: Colors.green), - ], - ), - smallGapV, - downloadButton, - ]; + final downloadSection = buildDownloadSectionUI( + defaultRemoteFileName: defaultRemoteFileName, + downloadFile: downloadFile, + downloadDone: downloadDone, + downloadButton: downloadButton, + ); // Widgets of the shared file download section - // Widgets of the file download section - final downloadSharedSection = [ - Text( - 'Download a shared large file from an external POD', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - smallGapV, - SizedBox( - width: 550, - child: TextFormField( - controller: sharedUrlController, - enabled: !(downloadSharedInProgress || downloadSharedDone), - decoration: const InputDecoration( - hintText: 'URL of shared large file in external POD', - hintStyle: TextStyle( - color: Colors.brown, - fontStyle: FontStyle.italic, - fontSize: 15, - ), - ), - ), - ), - smallGapV, - if (downloadSharedFile != null) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Save file'), - smallGapH, - Text( - downloadSharedFile!, - style: const TextStyle(color: Colors.blue), - ), - smallGapH, - if (downloadSharedDone) const Icon(Icons.done, color: Colors.green), - ], - ), - smallGapV, - downloadSharedButton, - ]; + final downloadSharedSection = buildDownloadSharedSectionUI( + downloadSharedFile: downloadSharedFile, + downloadSharedDone: downloadSharedDone, + downloadSharedInProgress: downloadSharedInProgress, + sharedUrlController: sharedUrlController, + downloadSharedButton: downloadSharedButton, + ); // Widgets of the file delete section - final deleteSection = [ - Text( - 'Delete the "$defaultRemoteFileName" from POD', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - smallGapV, - if (deleteInProgress || deleteDone) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Delete remote file'), - smallGapH, - Text( - defaultRemoteFileName, - style: const TextStyle(color: Colors.blue), - ), - smallGapH, - if (deleteDone) const Icon(Icons.done, color: Colors.green), - ], - ), - smallGapV, - deleteButton, - ]; + final deleteSection = buildDeleteSectionUI( + defaultRemoteFileName: defaultRemoteFileName, + deleteInProgress: deleteInProgress, + deleteDone: deleteDone, + deleteButton: deleteButton, + ); return Scaffold( body: Padding( @@ -599,7 +414,8 @@ class _FileServiceState extends State { top: 20, left: 0, right: 0, - child: getProgressBar('Uploading:', uploadDone, uploadPercent), + child: + buildProgressBar('Uploading:', uploadDone, uploadPercent), ), // Downloading progress bar @@ -609,7 +425,7 @@ class _FileServiceState extends State { top: 20, left: 0, right: 0, - child: getProgressBar( + child: buildProgressBar( 'Downloading:', downloadDone, downloadPercent), ), @@ -620,7 +436,7 @@ class _FileServiceState extends State { top: 20, left: 0, right: 0, - child: getProgressBar( + child: buildProgressBar( 'Downloading:', downloadSharedDone, downloadSharedPercent), ), @@ -631,7 +447,7 @@ class _FileServiceState extends State { top: 20, left: 0, right: 0, - child: getProgressBar('Deleting:', deleteDone, deletePercent), + child: buildProgressBar('Deleting:', deleteDone, deletePercent), ), // Navigate back to demo page diff --git a/example/lib/features/permission_callback_demo.dart b/example/lib/features/permission_callback_demo.dart index 5c48d4e1..5acd816a 100644 --- a/example/lib/features/permission_callback_demo.dart +++ b/example/lib/features/permission_callback_demo.dart @@ -28,6 +28,8 @@ import 'package:flutter/material.dart'; import 'package:solidpod/solidpod.dart'; import 'package:solidui/solidui.dart' show GrantPermissionUi; +import 'package:demopod/widgets/permission_demo_widgets.dart'; + /// A widget demonstrating the onPermissionGranted callback functionality. class PermissionCallbackDemo extends StatefulWidget { @@ -297,39 +299,7 @@ demo:exampleData$fileNumber children: [ // Header section. - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.blue[50], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue[200]!), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.lightbulb_outline, - color: Colors.blue[700], size: 28), - const SizedBox(width: 12), - const Text( - 'Why Use onPermissionGranted Callback?', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - ], - ), - const SizedBox(height: 12), - const Text( - 'The onPermissionGranted callback allows your app to automatically continue workflows after users grant permissions. This demo creates sample files automatically and shows how to share multiple files sequentially without manual navigation.', - style: TextStyle(fontSize: 16, height: 1.4), - ), - ], - ), - ), + buildDemoHeaderSection(), const SizedBox(height: 24), @@ -365,13 +335,13 @@ demo:exampleData$fileNumber Row( children: [ - _buildStepIndicator(1, 'Start', _currentStep >= 1), - _buildConnector(_currentStep >= 2), - _buildStepIndicator(2, 'Grant', _currentStep >= 2), - _buildConnector(_currentStep >= 3), - _buildStepIndicator(3, 'Process', _currentStep >= 3), - _buildConnector(_currentStep >= 4), - _buildStepIndicator(4, 'Complete', _currentStep >= 4), + buildStepIndicator(1, 'Start', _currentStep >= 1), + buildStepConnector(_currentStep >= 2), + buildStepIndicator(2, 'Grant', _currentStep >= 2), + buildStepConnector(_currentStep >= 3), + buildStepIndicator(3, 'Process', _currentStep >= 3), + buildStepConnector(_currentStep >= 4), + buildStepIndicator(4, 'Complete', _currentStep >= 4), ], ), @@ -581,47 +551,4 @@ demo:exampleData$fileNumber ), ); } - - Widget _buildStepIndicator(int step, String label, bool isActive) { - return Column( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: isActive ? Colors.blue[600] : Colors.grey[300], - shape: BoxShape.circle, - ), - child: Center( - child: Text( - step.toString(), - style: TextStyle( - color: isActive ? Colors.white : Colors.grey[600], - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 12, - color: isActive ? Colors.blue[600] : Colors.grey[600], - fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, - ), - ), - ], - ); - } - - Widget _buildConnector(bool isActive) { - return Container( - width: 24, - height: 2, - margin: const EdgeInsets.only(bottom: 20), - color: isActive ? Colors.blue[600] : Colors.grey[300], - ); - } } diff --git a/example/lib/features/view_keys.dart b/example/lib/features/view_keys.dart index bb0a88d8..03a9ba76 100644 --- a/example/lib/features/view_keys.dart +++ b/example/lib/features/view_keys.dart @@ -27,11 +27,11 @@ library; import 'package:flutter/material.dart'; +import 'package:solidpod/solidpod.dart' show KeyManager; + import 'package:demopod/constants/app.dart'; import 'package:demopod/utils/rdf.dart' show getEncKeyContent; -import 'package:solidpod/solidpod.dart' show KeyManager; - /// A widget to show the user all the encryption keys stored in their Solid Pod. class ViewKeys extends StatefulWidget { diff --git a/example/lib/home.dart b/example/lib/home.dart index aec5ed3f..75b4d05d 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -33,31 +33,27 @@ library; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:markdown_tooltip/markdown_tooltip.dart'; import 'package:solidpod/solidpod.dart'; import 'package:solidui/solidui.dart' show - GrantPermissionUi, - InitialSetupScreenBody, - SharedResourcesUi, changeKeyPopup, getKeyFromUserIfRequired, largeGapV, loginIfRequired, - logoutPopup, smallGapV; import 'package:demopod/constants/app.dart'; import 'package:demopod/dialogs/about.dart'; import 'package:demopod/dialogs/alert.dart'; +import 'package:demopod/dialogs/file_metadata.dart'; import 'package:demopod/features/create_acl_inherited_file.dart'; import 'package:demopod/features/edit_keyvalue.dart'; import 'package:demopod/features/file_service.dart'; -import 'package:demopod/features/permission_callback_demo.dart'; import 'package:demopod/features/read_acl_inherited_file.dart'; import 'package:demopod/features/view_keys.dart'; import 'package:demopod/main.dart'; import 'package:demopod/utils/rdf.dart'; +import 'package:demopod/widgets/home_sections.dart'; /// A widget for the demonstration screen of the application. @@ -230,58 +226,6 @@ class HomeState extends State with SingleTickerProviderStateMixin { } } - void showFileMetadataDialog({ - required BuildContext context, - required String fileName, - required String contentLength, - required String lastModified, - required String contentType, - required String allowdAccess, - }) { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('File Information'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _infoRow('File name', fileName), - _infoRow('Last modified', lastModified), - _infoRow('Content length', contentLength), - _infoRow('Content type', contentType), - _infoRow('Allowed operations', allowdAccess), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Close'), - ), - ], - ), - ); - } - - Widget _infoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 120, - child: Text( - '$label:', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - Expanded(child: Text(value)), - ], - ), - ); - } - Widget _build(BuildContext context, String title) { // Build the widget. @@ -584,288 +528,20 @@ class HomeState extends State with SingleTickerProviderStateMixin { ); }, ), - largeGapV, - const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Solid Server Login Management', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - MarkdownTooltip( - message: - 'This will remove from our local device\'s memory the ' - 'solid pod login information so that the next time you ' - 'start up the app you will need to login to your solid ' - 'server hosting your pod.', - child: ElevatedButton( - child: - const Text('Forget Remote Solid Server Login'), - onPressed: () async { - final deleteRes = await deleteLogIn(); - - var deleteMsg = ''; - - if (deleteRes) { - deleteMsg = - 'Successfully forgot remote solid server login info'; - } else { - deleteMsg = - 'Failed to forget login info. Try again in a while'; - } - - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Notice'), - content: Text(deleteMsg), - actions: [ - ElevatedButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('OK')) - ], - ), - ); - - _resetWebId(); - }, - ), - ), - smallGapV, - MarkdownTooltip( - message: - 'This will send a request through the browser to the ' - 'remote solid server to log you out of your Pod.', - child: ElevatedButton( - onPressed: () async { - await logoutPopup(context, const DemoPod()); - }, - child: - const Text('Logout From Remote Solid Server'), - ), - ), - largeGapV, - const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Resource Permission Management', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ElevatedButton( - child: const Text( - 'Add/Delete Permissions from a Specific Resource (key-value.ttl)'), - onPressed: () async { - final loggedIn = await loginIfRequired( - context, - ); - - if (loggedIn) { - await getKeyFromUserIfRequired(context, widget); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const GrantPermissionUi( - backgroundColor: titleBackgroundColor, - resourceName: 'keyvalue/key-value.ttl', - // accessModeList: ['read', 'write'], - // recipientTypeList: ['indi', 'group'], - // isFile: false, - child: Home(), - ), - ), - ); - } - }, - ), - smallGapV, - ElevatedButton( - child: const Text('Permission Callback Demo'), - onPressed: () async { - final loggedIn = await loginIfRequired( - context, - ); - - if (loggedIn) { - await getKeyFromUserIfRequired(context, widget); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const PermissionCallbackDemo( - child: Home(), - ), - ), - ); - } - }, - ), - smallGapV, - ElevatedButton( - child: const Text( - 'Add/Delete Permissions from any Resource'), - onPressed: () async { - final loggedIn = await loginIfRequired( - context, - ); - - if (loggedIn) { - await getKeyFromUserIfRequired(context, widget); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const GrantPermissionUi( - backgroundColor: titleBackgroundColor, - child: Home(), - ), - ), - ); - } - }, - ), - largeGapV, - const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Manage External Resources with Access', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ElevatedButton( - child: const Text( - 'View specific resource (key-value.ttl) your WebID has access to'), - onPressed: () async { - final loggedIn = await loginIfRequired( - context, - ); - - if (loggedIn) { - await getKeyFromUserIfRequired(context, widget); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SharedResourcesUi( - backgroundColor: titleBackgroundColor, - fileName: 'key-value.ttl', - child: Home(), - ), - ), - ); - } - }, - ), - smallGapV, - ElevatedButton( - child: const Text( - 'View ALL Resources your WebID has access to'), - onPressed: () async { - final loggedIn = await loginIfRequired( - context, - ); - - if (loggedIn) { - await getKeyFromUserIfRequired(context, widget); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SharedResourcesUi( - backgroundColor: titleBackgroundColor, - child: Home(), - ), - ), - ); - } - }, + ...buildLoginManagementSection( + context, + _resetWebId, + () => const DemoPod(), ), - smallGapV, - largeGapV, - const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Setup Wizard Demo', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ], + ...buildPermissionSection( + context, + widget, + () => const Home(), ), - smallGapV, - ElevatedButton( - onPressed: () async { - // Now that the back button issue is fixed in InitialSetupScreenBody, - // we can use it directly without any custom wrapper. - - final loggedIn = await loginIfRequired(context); - - if (!loggedIn) { - debugPrint('Please login to run the demo'); - return; - } - - final webId = await getWebId(); - if (webId == null) { - debugPrint('web ID is not available'); - return; - } - - final sampleDirUrl = await getDirUrl([ - await getDataDirPath(), - 'setup_wizard_demo', - ].join('/')); - final sampleFileName = 'setup_wizard_demo.ttl'; - final sampleFileUrl = await getFileUrl([ - await getDataDirPath(), - 'sampleFileName', - ].join('/')); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - body: SafeArea( - child: InitialSetupScreenBody( - // Sample resources that would need to be created. - - resNeedToCreate: { - 'folders': [sampleDirUrl], - 'files': [sampleFileUrl], - 'fileNames': [sampleFileName], - }, - child: const Home(), - ), - ), - ), - ), - ); - }, - child: const Text( - 'Show Solid Pod Setup Wizard (Using Real Component)'), + ...buildSetupWizardSection( + context, + () => const Home(), ), - smallGapV, ], ), ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 46935571..9346ef00 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,11 +27,12 @@ library; import 'package:flutter/material.dart'; -import 'package:demopod/home.dart'; -import 'package:demopod/utils/is_desktop.dart'; import 'package:solidui/solidui.dart' show SolidLogin, InfoButtonStyle; import 'package:window_manager/window_manager.dart'; +import 'package:demopod/home.dart'; +import 'package:demopod/utils/is_desktop.dart'; + void main() async { // Remove [debugPrint] messages from production code. diff --git a/example/lib/utils/rdf.dart b/example/lib/utils/rdf.dart index 08d27523..0c8dedb1 100644 --- a/example/lib/utils/rdf.dart +++ b/example/lib/utils/rdf.dart @@ -25,7 +25,6 @@ library; import 'package:rdflib/rdflib.dart'; - import 'package:solidpod/solidpod.dart' show getWebId; // Namespace for keys diff --git a/example/lib/widgets/file_service_sections.dart b/example/lib/widgets/file_service_sections.dart new file mode 100644 index 00000000..b3ee937a --- /dev/null +++ b/example/lib/widgets/file_service_sections.dart @@ -0,0 +1,282 @@ +/// Extracted UI sections for the FileService screen. +/// +/// Copyright (C) 2024, Software Innovation Institute, ANU. +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"). +/// +/// License: https://opensource.org/license/gpl-3-0. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . +/// +/// Authors: Dawei Chen + +library; + +import 'package:flutter/material.dart'; + +/// Builds a progress bar showing operation status. + +Widget buildProgressBar(String message, bool isDone, double percent) { + const smallGapH = SizedBox(width: 10); + const textStyle = TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ); + + final prefix = Text(message, style: textStyle); + final suffix = Text('${(percent * 100).toInt()}%', style: textStyle); + final progress = SizedBox( + width: 300, + height: 10, + child: LinearProgressIndicator( + value: percent, + minHeight: 2, + backgroundColor: Colors.black12, + color: Colors.greenAccent, + ), + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [prefix, smallGapH, progress, smallGapH, suffix], + ); +} + +/// Builds the upload section UI. + +List buildUploadSectionUI({ + required String defaultRemoteFileName, + required String? uploadFile, + required bool uploadDone, + required bool uploadInProgress, + required TextEditingController remoteFolderController, + required TextEditingController keyRefFolderController, + required Widget browseButton, + required Widget uploadButton, +}) { + const smallGapH = SizedBox(width: 10); + const smallGapV = SizedBox(height: 10); + + return [ + Text( + 'Upload a local large file and save it as "$defaultRemoteFileName" in POD', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + smallGapV, + Table( + columnWidths: const { + 0: FixedColumnWidth(450), + }, + children: [ + TableRow( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + uploadFile ?? + 'Click the Browse button to choose a local file', + style: TextStyle( + color: uploadFile == null ? Colors.red : Colors.blue, + fontStyle: FontStyle.italic, + fontSize: 16, + ), + ), + smallGapH, + if (uploadDone) const Icon(Icons.done, color: Colors.green), + ], + ), + ], + ), + TableRow( + children: [ + TextFormField( + controller: remoteFolderController, + enabled: !(uploadInProgress || uploadDone), + decoration: const InputDecoration( + hintText: '(Optional) save to folder in POD, e.g. dir1/dir2/', + hintStyle: TextStyle( + color: Colors.brown, + fontStyle: FontStyle.italic, + fontSize: 15, + ), + ), + ), + ], + ), + TableRow(children: [ + TextFormField( + controller: keyRefFolderController, + enabled: !(uploadInProgress || uploadDone), + decoration: const InputDecoration( + hintText: + '(Optional) Inherit encryption key of folder in POD, e.g. dir1/', + hintStyle: TextStyle( + color: Colors.brown, + fontStyle: FontStyle.italic, + fontSize: 15, + ), + ), + ), + ]), + ], + ), + smallGapV, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + browseButton, + smallGapH, + uploadButton, + ], + ), + ]; +} + +/// Builds the download section UI. + +List buildDownloadSectionUI({ + required String defaultRemoteFileName, + required String? downloadFile, + required bool downloadDone, + required Widget downloadButton, +}) { + const smallGapH = SizedBox(width: 10); + const smallGapV = SizedBox(height: 10); + + return [ + Text( + 'Download the "$defaultRemoteFileName" from POD', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + smallGapV, + if (downloadFile != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Save file'), + smallGapH, + Text( + downloadFile, + style: const TextStyle(color: Colors.blue), + ), + smallGapH, + if (downloadDone) const Icon(Icons.done, color: Colors.green), + ], + ), + smallGapV, + downloadButton, + ]; +} + +/// Builds the delete section UI. + +List buildDeleteSectionUI({ + required String defaultRemoteFileName, + required bool deleteInProgress, + required bool deleteDone, + required Widget deleteButton, +}) { + const smallGapH = SizedBox(width: 10); + const smallGapV = SizedBox(height: 10); + + return [ + Text( + 'Delete the "$defaultRemoteFileName" from POD', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + smallGapV, + if (deleteInProgress || deleteDone) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Delete remote file'), + smallGapH, + Text( + defaultRemoteFileName, + style: const TextStyle(color: Colors.blue), + ), + smallGapH, + if (deleteDone) const Icon(Icons.done, color: Colors.green), + ], + ), + smallGapV, + deleteButton, + ]; +} + +/// Builds the download shared file section UI. + +List buildDownloadSharedSectionUI({ + required String? downloadSharedFile, + required bool downloadSharedDone, + required bool downloadSharedInProgress, + required TextEditingController sharedUrlController, + required Widget downloadSharedButton, +}) { + const smallGapH = SizedBox(width: 10); + const smallGapV = SizedBox(height: 10); + + return [ + const Text( + 'Download a shared large file from an external POD', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + smallGapV, + SizedBox( + width: 550, + child: TextFormField( + controller: sharedUrlController, + enabled: !(downloadSharedInProgress || downloadSharedDone), + decoration: const InputDecoration( + hintText: 'URL of shared large file in external POD', + hintStyle: TextStyle( + color: Colors.brown, + fontStyle: FontStyle.italic, + fontSize: 15, + ), + ), + ), + ), + smallGapV, + if (downloadSharedFile != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Save file'), + smallGapH, + Text( + downloadSharedFile, + style: const TextStyle(color: Colors.blue), + ), + smallGapH, + if (downloadSharedDone) const Icon(Icons.done, color: Colors.green), + ], + ), + smallGapV, + downloadSharedButton, + ]; +} diff --git a/example/lib/widgets/home_sections.dart b/example/lib/widgets/home_sections.dart new file mode 100644 index 00000000..1c42fc3c --- /dev/null +++ b/example/lib/widgets/home_sections.dart @@ -0,0 +1,342 @@ +/// Extracted section widgets for the Home screen. +/// +/// Copyright (C) 2024, Software Innovation Institute, ANU. +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"). +/// +/// License: https://opensource.org/license/gpl-3-0. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . +/// +/// Authors: Dawei Chen + +// ignore_for_file: use_build_context_synchronously + +library; + +import 'package:flutter/material.dart'; + +import 'package:markdown_tooltip/markdown_tooltip.dart'; +import 'package:solidpod/solidpod.dart'; +import 'package:solidui/solidui.dart' + show + GrantPermissionUi, + InitialSetupScreenBody, + SharedResourcesUi, + getKeyFromUserIfRequired, + loginIfRequired, + logoutPopup, + largeGapV, + smallGapV; + +import 'package:demopod/constants/app.dart'; +import 'package:demopod/features/permission_callback_demo.dart'; + +/// Builds the login management section widgets. + +List buildLoginManagementSection( + BuildContext context, + VoidCallback onResetWebId, + Widget Function() createDemoPod, +) { + return [ + largeGapV, + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Solid Server Login Management', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + MarkdownTooltip( + message: 'This will remove from our local device\'s memory the ' + 'solid pod login information so that the next time you ' + 'start up the app you will need to login to your solid ' + 'server hosting your pod.', + child: ElevatedButton( + child: const Text('Forget Remote Solid Server Login'), + onPressed: () async { + final deleteRes = await deleteLogIn(); + + var deleteMsg = ''; + + if (deleteRes) { + deleteMsg = 'Successfully forgot remote solid server login info'; + } else { + deleteMsg = 'Failed to forget login info. Try again in a while'; + } + + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Notice'), + content: Text(deleteMsg), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('OK')) + ], + ), + ); + + onResetWebId(); + }, + ), + ), + smallGapV, + MarkdownTooltip( + message: 'This will send a request through the browser to the ' + 'remote solid server to log you out of your Pod.', + child: ElevatedButton( + onPressed: () async { + await logoutPopup(context, createDemoPod()); + }, + child: const Text('Logout From Remote Solid Server'), + ), + ), + ]; +} + +/// Builds the permission management and external resources section widgets. + +List buildPermissionSection( + BuildContext context, + Widget currentWidget, + Widget Function() createHomeWidget, +) { + return [ + largeGapV, + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Resource Permission Management', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ElevatedButton( + child: const Text( + 'Add/Delete Permissions from a Specific Resource (key-value.ttl)'), + onPressed: () async { + final loggedIn = await loginIfRequired( + context, + ); + + if (loggedIn) { + await getKeyFromUserIfRequired(context, currentWidget); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GrantPermissionUi( + backgroundColor: titleBackgroundColor, + resourceName: 'keyvalue/key-value.ttl', + // accessModeList: ['read', 'write'], + // recipientTypeList: ['indi', 'group'], + // isFile: false, + child: createHomeWidget(), + ), + ), + ); + } + }, + ), + smallGapV, + ElevatedButton( + child: const Text('Permission Callback Demo'), + onPressed: () async { + final loggedIn = await loginIfRequired( + context, + ); + + if (loggedIn) { + await getKeyFromUserIfRequired(context, currentWidget); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + PermissionCallbackDemo(child: createHomeWidget()), + ), + ); + } + }, + ), + smallGapV, + ElevatedButton( + child: const Text('Add/Delete Permissions from any Resource'), + onPressed: () async { + final loggedIn = await loginIfRequired( + context, + ); + + if (loggedIn) { + await getKeyFromUserIfRequired(context, currentWidget); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GrantPermissionUi( + backgroundColor: titleBackgroundColor, + child: createHomeWidget(), + ), + ), + ); + } + }, + ), + largeGapV, + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Manage External Resources with Access', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ElevatedButton( + child: const Text( + 'View specific resource (key-value.ttl) your WebID has access to'), + onPressed: () async { + final loggedIn = await loginIfRequired( + context, + ); + + if (loggedIn) { + await getKeyFromUserIfRequired(context, currentWidget); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SharedResourcesUi( + backgroundColor: titleBackgroundColor, + fileName: 'key-value.ttl', + child: createHomeWidget(), + ), + ), + ); + } + }, + ), + smallGapV, + ElevatedButton( + child: const Text('View ALL Resources your WebID has access to'), + onPressed: () async { + final loggedIn = await loginIfRequired( + context, + ); + + if (loggedIn) { + await getKeyFromUserIfRequired(context, currentWidget); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SharedResourcesUi( + backgroundColor: titleBackgroundColor, + child: createHomeWidget(), + ), + ), + ); + } + }, + ), + smallGapV, + ]; +} + +/// Builds the setup wizard section widgets. + +List buildSetupWizardSection( + BuildContext context, + Widget Function() createHomeWidget, +) { + return [ + largeGapV, + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Setup Wizard Demo', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + smallGapV, + ElevatedButton( + onPressed: () async { + final loggedIn = await loginIfRequired(context); + + if (!loggedIn) { + debugPrint('Please login to run the demo'); + return; + } + + final webId = await getWebId(); + if (webId == null) { + debugPrint('web ID is not available'); + return; + } + + final sampleDirUrl = await getDirUrl([ + await getDataDirPath(), + 'setup_wizard_demo', + ].join('/')); + final sampleFileName = 'setup_wizard_demo.ttl'; + final sampleFileUrl = await getFileUrl([ + await getDataDirPath(), + 'sampleFileName', + ].join('/')); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: SafeArea( + child: InitialSetupScreenBody( + resNeedToCreate: { + 'folders': [sampleDirUrl], + 'files': [sampleFileUrl], + 'fileNames': [sampleFileName], + }, + child: createHomeWidget(), + ), + ), + ), + ), + ); + }, + child: const Text('Show Solid Pod Setup Wizard (Using Real Component)'), + ), + smallGapV, + ]; +} diff --git a/example/lib/widgets/permission_demo_widgets.dart b/example/lib/widgets/permission_demo_widgets.dart new file mode 100644 index 00000000..517b549d --- /dev/null +++ b/example/lib/widgets/permission_demo_widgets.dart @@ -0,0 +1,113 @@ +/// Extracted widgets for the Permission Callback Demo screen. +/// +/// Copyright (C) 2025, Software Innovation Institute, ANU. +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"). +/// +/// License: https://opensource.org/license/gpl-3-0. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . +/// +/// Authors: Dawei Chen + +library; + +import 'package:flutter/material.dart'; + +/// Builds a step indicator for the workflow progress display. + +Widget buildStepIndicator(int step, String label, bool isActive) { + return Column( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isActive ? Colors.blue[600] : Colors.grey[300], + shape: BoxShape.circle, + ), + child: Center( + child: Text( + step.toString(), + style: TextStyle( + color: isActive ? Colors.white : Colors.grey[600], + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isActive ? Colors.blue[600] : Colors.grey[600], + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ); +} + +/// Builds a connector line between step indicators. + +Widget buildStepConnector(bool isActive) { + return Container( + width: 24, + height: 2, + margin: const EdgeInsets.only(bottom: 20), + color: isActive ? Colors.blue[600] : Colors.grey[300], + ); +} + +/// Builds the header section explaining the demo purpose. + +Widget buildDemoHeaderSection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue[200]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.lightbulb_outline, color: Colors.blue[700], size: 28), + const SizedBox(width: 12), + const Text( + 'Why Use onPermissionGranted Callback?', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 12), + const Text( + 'The onPermissionGranted callback allows your app to automatically ' + 'continue workflows after users grant permissions. This demo creates ' + 'sample files automatically and shows how to share multiple files ' + 'sequentially without manual navigation.', + style: TextStyle(fontSize: 16, height: 1.4), + ), + ], + ), + ); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d16aff7d..fafcd772 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -28,6 +28,7 @@ dependency_overrides: ref: dev dev_dependencies: + dependency_validator: ^5.0.4 flutter_lints: ^6.0.0 flutter: diff --git a/example/support/flutter.mk b/example/support/flutter.mk index e8d2168f..7e7cf942 100644 --- a/example/support/flutter.mk +++ b/example/support/flutter.mk @@ -174,7 +174,7 @@ analyze: .PHONY: depend depend: @echo "Review pubspec.yaml dependencies." - -dependency_validator + -dart run dependency_validator @echo $(SEPARATOR) .PHONY: ignore diff --git a/pubspec.yaml b/pubspec.yaml index ce0e0808..cca5bb89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: dev_dependencies: build_runner: ^2.10.5 custom_lint: ^0.8.1 + dependency_validator: ^5.0.4 flutter_lints: ^6.0.0 import_order_lint: ^0.2.2 diff --git a/support/flutter.mk b/support/flutter.mk index 5768fdea..ec907b92 100644 --- a/support/flutter.mk +++ b/support/flutter.mk @@ -213,7 +213,7 @@ analyze: .PHONY: depend depend: @echo "Dart: REVIEW DEPENDENCIES." - -dependency_validator + -dart run dependency_validator @echo $(SEPARATOR) # Check and fail if any files exceed limit.