From 5cda0848d6653629e8fc0c4a0b86a995aecf6e6e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 5 Feb 2026 21:15:17 +1100 Subject: [PATCH 01/21] =?UTF-8?q?Add=20a=20popup=20to=20inform=20users=20t?= =?UTF-8?q?hat=20the=20file=20they=20are=20about=20to=20download=20is=20lo?= =?UTF-8?q?cated=20in=20another=20application=E2=80=99s=20data=20folder=20?= =?UTF-8?q?and=20cannot=20be=20decrypted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/solid_file_operations_download.dart | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 79739a0..5437d34 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -43,6 +43,102 @@ import 'package:solidui/src/utils/solid_pod_helpers.dart'; class SolidFileDownloadOperations { const SolidFileDownloadOperations._(); + /// Checks if a file is within the current app's data folder. + /// + /// Returns `true` if the file path starts with the app's data directory path, + /// indicating that the current app can decrypt this file. + /// Returns `false` if the file is from another app's folder, meaning + /// decryption may fail as the security key is not available. + + static Future _isFileInCurrentAppFolder(String filePath) async { + try { + final appDataPath = await getDataDirPath(); + if (appDataPath.isEmpty) { + // If no app data path is available, we cannot determine ownership. + + return false; + } + + // Normalise the file path by removing leading slashes for comparison. + + final normalisedFilePath = + filePath.startsWith('/') ? filePath.substring(1) : filePath; + + return normalisedFilePath.startsWith(appDataPath); + } catch (e) { + debugPrint('Error checking app folder ownership: $e'); + return false; + } + } + + /// Shows a warning dialogue when attempting to download an encrypted file + /// from another app's data folder. + /// + /// Returns `true` if the user chooses to proceed with the download. + /// Returns `false` if the user cancels. + + static Future _showCrossAppDecryptionWarning( + BuildContext context, + ) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28), + SizedBox(width: 12), + Expanded(child: Text('Decryption Warning')), + ], + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'This encrypted file belongs to another application\'s data ' + 'folder.', + style: TextStyle(fontWeight: FontWeight.w500), + ), + SizedBox(height: 12), + Text( + 'The file browser can browse files across all app folders in ' + 'your POD, but can only decrypt files within the current app\'s ' + 'data folder.', + ), + SizedBox(height: 12), + Text( + 'Since this file was encrypted by a different application, ' + 'the security key required to decrypt it is not available. ' + 'The downloaded file will likely be unreadable or corrupted.', + ), + SizedBox(height: 16), + Text( + 'Do you still wish to proceed with the download?', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + child: const Text('Download Anyway'), + ), + ], + ), + ); + + return result ?? false; + } + /// Default file download implementation. static Future downloadFile( @@ -52,6 +148,27 @@ class SolidFileDownloadOperations { PathType? pathType, }) async { try { + // Check if the file is an encrypted file from another app's folder. + // If so, warn the user that decryption may not be possible. + + final isEncryptedFile = fileName.endsWith('.enc.ttl'); + + if (isEncryptedFile) { + final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); + + if (!isInCurrentAppFolder) { + if (!context.mounted) return; + + final shouldProceed = await _showCrossAppDecryptionWarning(context); + + if (!shouldProceed) { + return; + } + } + } + + if (!context.mounted) return; + // Let user choose where to save the file. final cleanFileName = fileName.replaceAll('.enc.ttl', ''); From 6ef5cadcf5094f00e45ee0dad0d9d174b006c017 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 5 Feb 2026 21:53:40 +1100 Subject: [PATCH 02/21] Fix the issue that uploaded file appears in the unexpected directory --- lib/src/utils/solid_file_operations.dart | 2 ++ .../utils/solid_file_operations_upload.dart | 34 +++++++++++++++---- lib/src/widgets/solid_file.dart | 1 + lib/src/widgets/solid_file_callbacks.dart | 6 ++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index 27f00ba..97a0c5e 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -78,11 +78,13 @@ class SolidFileOperations { static Future uploadFile( BuildContext context, String currentPath, { + String? basePath, VoidCallback? onSuccess, }) => SolidFileUploadOperations.uploadFile( context, currentPath, + basePath: basePath, onSuccess: onSuccess, ); } diff --git a/lib/src/utils/solid_file_operations_upload.dart b/lib/src/utils/solid_file_operations_upload.dart index 190d087..f4c82b9 100644 --- a/lib/src/utils/solid_file_operations_upload.dart +++ b/lib/src/utils/solid_file_operations_upload.dart @@ -49,6 +49,7 @@ class SolidFileUploadOperations { static Future uploadFile( BuildContext context, String currentPath, { + String? basePath, VoidCallback? onSuccess, }) async { try { @@ -102,16 +103,37 @@ class SolidFileUploadOperations { final remoteFileName = '$sanitizedFileName.enc.ttl'; - // Determine upload path. + // Calculate upload path relative to the base path. String uploadPath = remoteFileName; if (currentPath.isNotEmpty && currentPath != '/') { - // Remove leading slash if present. + String relativePath = currentPath; - final cleanPath = currentPath.startsWith('/') - ? currentPath.substring(1) - : currentPath; - uploadPath = '$cleanPath/$remoteFileName'; + // Strip the base path from current path to get the relative path. + + if (basePath != null && basePath.isNotEmpty) { + if (currentPath.startsWith(basePath)) { + relativePath = currentPath.substring(basePath.length); + + // Remove leading slash if present after stripping. + + if (relativePath.startsWith('/')) { + relativePath = relativePath.substring(1); + } + } + } else { + // Remove leading slash if present. + + if (relativePath.startsWith('/')) { + relativePath = relativePath.substring(1); + } + } + + // Only prepend relative path if it's not empty. + + if (relativePath.isNotEmpty) { + uploadPath = '$relativePath/$remoteFileName'; + } } if (!context.mounted) return; diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index f50b61c..cbbd2aa 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -283,6 +283,7 @@ class _SolidFileState extends State { context, _currentPath, _browserKey, + basePath: _effectiveBasePath, ); } diff --git a/lib/src/widgets/solid_file_callbacks.dart b/lib/src/widgets/solid_file_callbacks.dart index 2c6454f..f875dad 100644 --- a/lib/src/widgets/solid_file_callbacks.dart +++ b/lib/src/widgets/solid_file_callbacks.dart @@ -42,13 +42,15 @@ class SolidFileDefaultCallbacks { static SolidFileUploadCallbacks createUploadCallbacks( BuildContext context, String currentPath, - GlobalKey browserKey, - ) { + GlobalKey browserKey, { + String? basePath, + }) { return SolidFileUploadCallbacks( onUpload: () { SolidFileOperations.uploadFile( context, currentPath, + basePath: basePath, onSuccess: () { // Refresh the file browser after successful upload. From 9583f73d1a3b294bf7f719cbd45c86f79f0840f4 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 02:06:58 +1100 Subject: [PATCH 03/21] Refactor file-browsing code built upon basePath --- lib/src/models/file_type_config.dart | 36 +++- lib/src/utils/path_utils.dart | 188 ++++++++++++++++++ lib/src/utils/solid_file_operations.dart | 4 +- .../utils/solid_file_operations_delete.dart | 14 +- .../utils/solid_file_operations_download.dart | 16 +- .../utils/solid_file_operations_upload.dart | 47 ++--- lib/src/widgets/solid_file.dart | 23 ++- lib/src/widgets/solid_file_browser.dart | 46 +++-- .../widgets/solid_file_browser_builder.dart | 16 +- lib/src/widgets/solid_file_callbacks.dart | 11 +- lib/src/widgets/solid_file_helpers.dart | 24 ++- lib/src/widgets/solid_file_operations.dart | 46 +++-- 12 files changed, 365 insertions(+), 106 deletions(-) create mode 100644 lib/src/utils/path_utils.dart diff --git a/lib/src/models/file_type_config.dart b/lib/src/models/file_type_config.dart index 674d716..3ad21ba 100644 --- a/lib/src/models/file_type_config.dart +++ b/lib/src/models/file_type_config.dart @@ -29,6 +29,7 @@ library; import 'package:solidui/src/models/data_format_config.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_helpers.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; @@ -97,7 +98,13 @@ class FileTypeConfig { /// Gets the file type configuration based on the current path. static FileTypeConfig fromPath(String currentPath, [String? basePath]) { - if (currentPath.contains('/blood_pressure')) { + // Normalise the path for consistent pattern matching. + + final normalisedPath = PathUtils.normalise(currentPath); + + if (normalisedPath.contains('/blood_pressure') || + normalisedPath.contains('blood_pressure/') || + normalisedPath.endsWith('blood_pressure')) { return const FileTypeConfig( type: SolidFileType.bloodPressure, displayName: 'Blood Pressure Data', @@ -109,7 +116,9 @@ class FileTypeConfig { ''', ); - } else if (currentPath.contains('/vaccination')) { + } else if (normalisedPath.contains('/vaccination') || + normalisedPath.contains('vaccination/') || + normalisedPath.endsWith('vaccination')) { return const FileTypeConfig( type: SolidFileType.vaccination, displayName: 'Vaccination Data', @@ -121,7 +130,9 @@ class FileTypeConfig { ''', ); - } else if (currentPath.contains('/medication')) { + } else if (normalisedPath.contains('/medication') || + normalisedPath.contains('medication/') || + normalisedPath.endsWith('medication')) { return const FileTypeConfig( type: SolidFileType.medication, displayName: 'Medication Data', @@ -133,7 +144,9 @@ class FileTypeConfig { ''', ); - } else if (currentPath.contains('/diary')) { + } else if (normalisedPath.contains('/diary') || + normalisedPath.contains('diary/') || + normalisedPath.endsWith('diary')) { return const FileTypeConfig( type: SolidFileType.diary, displayName: 'Appointments Data', @@ -145,7 +158,9 @@ class FileTypeConfig { ''', ); - } else if (currentPath.contains('/profile')) { + } else if (normalisedPath.contains('/profile') || + normalisedPath.contains('profile/') || + normalisedPath.endsWith('profile')) { return const FileTypeConfig( type: SolidFileType.profile, displayName: 'Profile Data', @@ -162,24 +177,25 @@ class FileTypeConfig { // consistency. If basePath is provided, use it; otherwise, construct a // reasonable default. - String effectiveBasePath = basePath ?? ''; + String effectiveBasePath = + basePath != null ? PathUtils.normalise(basePath) : ''; if (effectiveBasePath.isEmpty) { final segments = - currentPath.split('/').where((s) => s.isNotEmpty).toList(); + normalisedPath.split('/').where((s) => s.isNotEmpty).toList(); // Construct a reasonable base path - typically the first 2 segments // for most cases. if (segments.length >= 2) { - effectiveBasePath = '/${segments[0]}/${segments[1]}'; + effectiveBasePath = '${segments[0]}/${segments[1]}'; } else if (segments.length == 1) { - effectiveBasePath = '/${segments[0]}'; + effectiveBasePath = segments[0]; } } final friendlyName = SolidFileHelpers.getFriendlyFolderName( - currentPath, + normalisedPath, effectiveBasePath, ); diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart new file mode 100644 index 0000000..f936a2a --- /dev/null +++ b/lib/src/utils/path_utils.dart @@ -0,0 +1,188 @@ +/// Path utilities for SolidUI. +/// +/// 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: Tony Chen + +library; + +/// Utility class for path operations in SolidUI. +/// +/// This class provides methods to normalise and manipulate paths used in +/// file browsing operations. All paths are treated as relative to the Pod +/// root and should not have leading forward slashes. +/// +/// The `basePath` concept (used in earlier versions of solidpod) has been +/// replaced with consistent use of `PathType.relativeToPod` from solidpod. +/// This ensures that paths like `myapp/data/file.ttl` are correctly resolved +/// without causing double slashes in URLs. + +class PathUtils { + const PathUtils._(); + + /// Normalises a relative path by removing leading and trailing slashes. + /// + /// This ensures paths are consistently formatted for use with solidpod's + /// `PathType.relativeToPod` option, which expects paths without leading + /// slashes. + /// + /// Examples: + /// - `/myapp/data` becomes `myapp/data` + /// - `myapp/data/` becomes `myapp/data` + /// - `//myapp//data//` becomes `myapp/data` + /// - `` remains `` + /// - `/` becomes `` + + static String normalise(String path) { + if (path.isEmpty) return ''; + + // Remove leading slashes. + + String normalised = path; + while (normalised.startsWith('/')) { + normalised = normalised.substring(1); + } + + // Remove trailing slashes. + + while (normalised.endsWith('/')) { + normalised = normalised.substring(0, normalised.length - 1); + } + + // Remove consecutive slashes. + + normalised = normalised.replaceAll(RegExp(r'/+'), '/'); + + return normalised; + } + + /// Joins path segments into a normalised path. + /// + /// All segments are normalised and empty segments are filtered out. + /// + /// Examples: + /// - join(['myapp', 'data', 'file.ttl']) returns `myapp/data/file.ttl` + /// - join(['/myapp/', '/data/', 'file.ttl']) returns `myapp/data/file.ttl` + /// - join(['', 'myapp', '', 'data']) returns `myapp/data` + + static String join(List segments) { + final normalisedSegments = segments + .map(normalise) + .where((s) => s.isNotEmpty) + .toList(); + + return normalisedSegments.join('/'); + } + + /// Extracts the relative path from a full path given a root path. + /// + /// Both paths are normalised before comparison. If the full path does not + /// start with the root path, the full normalised path is returned. + /// + /// Examples: + /// - relativeTo('myapp/data/subfolder', 'myapp/data') returns `subfolder` + /// - relativeTo('/myapp/data/subfolder', '/myapp/data') returns `subfolder` + /// - relativeTo('myapp/data', 'myapp/data') returns `` + /// - relativeTo('other/path', 'myapp/data') returns `other/path` + + static String relativeTo(String fullPath, String rootPath) { + final normalisedFull = normalise(fullPath); + final normalisedRoot = normalise(rootPath); + + if (normalisedRoot.isEmpty) { + return normalisedFull; + } + + if (normalisedFull == normalisedRoot) { + return ''; + } + + if (normalisedFull.startsWith('$normalisedRoot/')) { + return normalisedFull.substring(normalisedRoot.length + 1); + } + + return normalisedFull; + } + + /// Combines a directory path and a file name into a full path. + /// + /// Both are normalised before joining. + /// + /// Examples: + /// - combine('myapp/data', 'file.ttl') returns `myapp/data/file.ttl` + /// - combine('/myapp/data/', '/file.ttl') returns `myapp/data/file.ttl` + /// - combine('', 'file.ttl') returns `file.ttl` + + static String combine(String directoryPath, String fileName) { + return join([directoryPath, fileName]); + } + + /// Checks if a path is the root (empty or just slashes). + /// + /// Examples: + /// - isRoot('') returns true + /// - isRoot('/') returns true + /// - isRoot('//') returns true + /// - isRoot('myapp') returns false + + static bool isRoot(String path) { + return normalise(path).isEmpty; + } + + /// Gets the parent directory of a path. + /// + /// Returns empty string if the path has no parent (is root or single + /// segment). + /// + /// Examples: + /// - parent('myapp/data/subfolder') returns `myapp/data` + /// - parent('myapp') returns `` + /// - parent('') returns `` + + static String parent(String path) { + final normalised = normalise(path); + final lastSlash = normalised.lastIndexOf('/'); + if (lastSlash == -1) { + return ''; + } + return normalised.substring(0, lastSlash); + } + + /// Gets the last segment (file or directory name) of a path. + /// + /// Examples: + /// - basename('myapp/data/file.ttl') returns `file.ttl` + /// - basename('myapp') returns `myapp` + /// - basename('') returns `` + + static String basename(String path) { + final normalised = normalise(path); + final lastSlash = normalised.lastIndexOf('/'); + if (lastSlash == -1) { + return normalised; + } + return normalised.substring(lastSlash + 1); + } +} diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index 97a0c5e..ca96c1d 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -62,6 +62,7 @@ class SolidFileOperations { BuildContext context, String fileName, String filePath, { + @Deprecated('basePath is no longer used. Paths are relative to Pod root.') String? basePath, VoidCallback? onSuccess, }) => @@ -69,7 +70,6 @@ class SolidFileOperations { context, fileName, filePath, - basePath: basePath, onSuccess: onSuccess, ); @@ -78,13 +78,13 @@ class SolidFileOperations { static Future uploadFile( BuildContext context, String currentPath, { + @Deprecated('basePath is no longer used. Paths are relative to Pod root.') String? basePath, VoidCallback? onSuccess, }) => SolidFileUploadOperations.uploadFile( context, currentPath, - basePath: basePath, onSuccess: onSuccess, ); } diff --git a/lib/src/utils/solid_file_operations_delete.dart b/lib/src/utils/solid_file_operations_delete.dart index 744a32e..1e541a2 100644 --- a/lib/src/utils/solid_file_operations_delete.dart +++ b/lib/src/utils/solid_file_operations_delete.dart @@ -32,17 +32,26 @@ import 'package:flutter/material.dart'; import 'package:solidpod/solidpod.dart'; +import 'package:solidui/src/utils/path_utils.dart'; + /// Delete operations for SolidUI widgets. class SolidFileDeleteOperations { const SolidFileDeleteOperations._(); /// Default file deletion implementation. + /// + /// The [filePath] should be a directory path relative to the Pod root, + /// e.g., `myapp/data` or `myapp/data/subfolder`. + /// + /// Note: The [basePath] parameter is deprecated and will be ignored. + /// File paths are now handled using PathType.relativeToPod instead. static Future deletePodFile( BuildContext context, String fileName, String filePath, { + @Deprecated('basePath is no longer used. Paths are relative to Pod root.') String? basePath, VoidCallback? onSuccess, }) async { @@ -92,9 +101,10 @@ class SolidFileDeleteOperations { try { // Construct the full file path by combining directory path and - // filename. + // filename. Use PathUtils to ensure no leading slashes, which would + // cause double slashes in the generated URL. - final fullFilePath = [filePath, fileName].join('/'); + final fullFilePath = PathUtils.combine(filePath, fileName); // Delete the file (this also handles the ACL file automatically). diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 79739a0..7408932 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -36,6 +36,7 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:solidpod/solidpod.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/utils/solid_pod_helpers.dart'; /// Download operations for SolidUI widgets. @@ -96,20 +97,9 @@ class SolidFileDownloadOperations { // Read file content from POD. - // dc 20260107: the `basePath` is heavily involved in the file-browsing - // codebase, and this leads to a leading forward slash in `filePath`, - // e.g., /myapp/encryption/ind-keys.ttl. - // This format triggers an error when extracting data from the turtle - // content due to double `//` in the subject of triples. - // Below is a temporary workaround but a better solution is needed to - // fully resolve this issue (e.g., refactor the file-browsing code to - // use `PathType` instead of `basePath`). - + final normalisedPath = PathUtils.combine(filePath, fileName); final fileContent = await readPod( - [ - filePath.startsWith('/') ? filePath.substring(1) : filePath, - fileName, - ].join('/'), + normalisedPath, pathType: pathType ?? PathType.relativeToPod, ); diff --git a/lib/src/utils/solid_file_operations_upload.dart b/lib/src/utils/solid_file_operations_upload.dart index f4c82b9..b4142a1 100644 --- a/lib/src/utils/solid_file_operations_upload.dart +++ b/lib/src/utils/solid_file_operations_upload.dart @@ -38,6 +38,7 @@ import 'package:path/path.dart' as path; import 'package:solidpod/solidpod.dart'; import 'package:solidui/src/utils/is_text_file.dart'; +import 'package:solidui/src/utils/path_utils.dart'; /// Upload operations for SolidUI widgets. @@ -49,6 +50,7 @@ class SolidFileUploadOperations { static Future uploadFile( BuildContext context, String currentPath, { + @Deprecated('basePath is no longer used. Paths are relative to Pod root.') String? basePath, VoidCallback? onSuccess, }) async { @@ -103,44 +105,23 @@ class SolidFileUploadOperations { final remoteFileName = '$sanitizedFileName.enc.ttl'; - // Calculate upload path relative to the base path. + // Construct the full upload path relative to the Pod root. - String uploadPath = remoteFileName; - if (currentPath.isNotEmpty && currentPath != '/') { - String relativePath = currentPath; - - // Strip the base path from current path to get the relative path. - - if (basePath != null && basePath.isNotEmpty) { - if (currentPath.startsWith(basePath)) { - relativePath = currentPath.substring(basePath.length); - - // Remove leading slash if present after stripping. - - if (relativePath.startsWith('/')) { - relativePath = relativePath.substring(1); - } - } - } else { - // Remove leading slash if present. - - if (relativePath.startsWith('/')) { - relativePath = relativePath.substring(1); - } - } - - // Only prepend relative path if it's not empty. - - if (relativePath.isNotEmpty) { - uploadPath = '$relativePath/$remoteFileName'; - } - } + final normalisedCurrentPath = PathUtils.normalise(currentPath); + final uploadPath = normalisedCurrentPath.isNotEmpty + ? PathUtils.combine(normalisedCurrentPath, remoteFileName) + : remoteFileName; if (!context.mounted) return; - // Upload file with encryption. + // Upload file with encryption using PathType.relativeToPod. - await writePod(uploadPath, fileContent, encrypted: true); + await writePod( + uploadPath, + fileContent, + encrypted: true, + pathType: PathType.relativeToPod, + ); if (!context.mounted) return; diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index cbbd2aa..096f3fe 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -32,6 +32,7 @@ import 'package:flutter/material.dart'; import 'package:solidpod/solidpod.dart' show getDataDirPath; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; import 'package:solidui/src/widgets/solid_file_browser_builder.dart'; import 'package:solidui/src/widgets/solid_file_callbacks.dart'; @@ -216,10 +217,12 @@ class _SolidFileState extends State { Future _resolveBasePath() async { if (widget.basePath != null) { - // Use the provided basePath directly. + // Use the provided basePath directly, normalised. - _resolvedBasePath = widget.basePath; - _currentPath = widget.currentPath ?? _resolvedBasePath!; + _resolvedBasePath = PathUtils.normalise(widget.basePath!); + _currentPath = widget.currentPath != null + ? PathUtils.normalise(widget.currentPath!) + : _resolvedBasePath!; setState(() { _isResolvingBasePath = false; }); @@ -230,7 +233,7 @@ class _SolidFileState extends State { try { final appDataPath = await getDataDirPath(); - _resolvedBasePath = appDataPath; + _resolvedBasePath = PathUtils.normalise(appDataPath); } catch (e) { // Fall back to pod root if getDataDirPath fails. @@ -238,7 +241,9 @@ class _SolidFileState extends State { _resolvedBasePath = SolidFile.podRoot; } - _currentPath = widget.currentPath ?? _resolvedBasePath!; + _currentPath = widget.currentPath != null + ? PathUtils.normalise(widget.currentPath!) + : _resolvedBasePath!; setState(() { _isResolvingBasePath = false; }); @@ -283,20 +288,22 @@ class _SolidFileState extends State { context, _currentPath, _browserKey, - basePath: _effectiveBasePath, ); } /// Handles directory changes and updates internal state. void _handleDirectoryChanged(String path) { + // Normalise the path to ensure consistent handling. + + final normalisedPath = PathUtils.normalise(path); setState(() { - _currentPath = path; + _currentPath = normalisedPath; }); // Call the external callback if provided. - widget.onDirectoryChanged?.call(path); + widget.onDirectoryChanged?.call(normalisedPath); } @override diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart index b8fd1cf..d58ddf4 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -34,6 +34,7 @@ import 'package:solidpod/solidpod.dart'; import 'package:solidui/src/models/file_item.dart'; import 'package:solidui/src/utils/file_operations.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_browser_content.dart'; import 'package:solidui/src/widgets/solid_file_browser_loading_state.dart'; import 'package:solidui/src/widgets/solid_file_browser_not_logged_in.dart'; @@ -138,8 +139,10 @@ class SolidFileBrowserState extends State { @override void initState() { super.initState(); - currentPath = widget.basePath; - pathHistory = [widget.basePath]; + // Normalise the base path to ensure consistent path handling. + + currentPath = PathUtils.normalise(widget.basePath); + pathHistory = [currentPath]; _checkLoginStatus(); } @@ -178,7 +181,10 @@ class SolidFileBrowserState extends State { Future navigateToDirectory(String dirName) async { if (!mounted) return; setState(() { - currentPath = '$currentPath/$dirName'; + // Use PathUtils.combine to ensure consistent path joining without + // double slashes. + + currentPath = PathUtils.combine(currentPath, dirName); pathHistory.add(currentPath); }); await refreshFiles(); @@ -265,32 +271,42 @@ class SolidFileBrowserState extends State { /// Navigate to a specific path in the file browser. void navigateToPath(String path) { + // Normalise both the target path and the base path to ensure consistent + // comparison and prevent issues with leading slashes. + + final normalisedPath = PathUtils.normalise(path); + final normalisedBasePath = PathUtils.normalise(widget.basePath); + setState(() { - currentPath = path; - if (path == widget.basePath) { - pathHistory = [widget.basePath]; + currentPath = normalisedPath; + if (normalisedPath == normalisedBasePath || normalisedPath.isEmpty) { + pathHistory = [normalisedBasePath]; } else { - if (pathHistory.isEmpty || pathHistory.last != path) { - if (path.startsWith(widget.basePath)) { - pathHistory = [widget.basePath]; - final relativePath = path.substring(widget.basePath.length); - if (relativePath.isNotEmpty && relativePath != '/') { + if (pathHistory.isEmpty || pathHistory.last != normalisedPath) { + // Check if the path is under the base path. + + if (normalisedBasePath.isEmpty || + normalisedPath.startsWith('$normalisedBasePath/')) { + pathHistory = [normalisedBasePath]; + final relativePath = + PathUtils.relativeTo(normalisedPath, normalisedBasePath); + if (relativePath.isNotEmpty) { final segments = relativePath.split('/').where((s) => s.isNotEmpty); - var currentBuildPath = widget.basePath; + var currentBuildPath = normalisedBasePath; for (final segment in segments) { - currentBuildPath = [currentBuildPath, segment].join('/'); + currentBuildPath = PathUtils.combine(currentBuildPath, segment); pathHistory.add(currentBuildPath); } } } else { - pathHistory.add(path); + pathHistory.add(normalisedPath); } } } refreshFiles(); }); - widget.onDirectoryChanged.call(path); + widget.onDirectoryChanged.call(normalisedPath); } /// Gets the effective friendly folder name based on the current path. diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index 6dccc54..ff5f25d 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -32,6 +32,7 @@ import 'package:flutter/material.dart'; import 'package:solidpod/solidpod.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/utils/solid_file_operations.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; @@ -52,10 +53,14 @@ class SolidFileBrowserBuilder { required Function(String path) onDirectoryChanged, SolidFileUploadCallbacks? uploadCallbacks, }) { + // Normalise the base path to ensure consistent path handling. + + final normalisedBasePath = PathUtils.normalise(basePath); + return SolidFileBrowser( key: browserKey, browserKey: browserKey, - basePath: basePath, + basePath: normalisedBasePath, friendlyFolderName: friendlyFolderName, onFileSelected: onFileSelected ?? (fileName, filePath) { @@ -63,20 +68,25 @@ class SolidFileBrowserBuilder { }, onFileDownload: onFileDownload ?? (fileName, filePath) { + // Use PathType.relativeToPod for all paths as they are now + // consistently normalised (no leading slashes). + SolidFileOperations.downloadFile( browserKey.currentContext!, fileName, filePath, - pathType: basePath.trim().isEmpty ? PathType.relativeToPod : null, + pathType: PathType.relativeToPod, ); }, onFileDelete: onFileDelete ?? (fileName, filePath) { + // filePath is already relative to Pod root (e.g., 'myapp/data/subfolder'). + // No basePath conversion needed as we use PathType.relativeToPod approach. + SolidFileOperations.deletePodFile( browserKey.currentContext!, fileName, filePath, - basePath: basePath, onSuccess: () { // Refresh the browser after successful deletion. diff --git a/lib/src/widgets/solid_file_callbacks.dart b/lib/src/widgets/solid_file_callbacks.dart index f875dad..946b97a 100644 --- a/lib/src/widgets/solid_file_callbacks.dart +++ b/lib/src/widgets/solid_file_callbacks.dart @@ -30,6 +30,7 @@ library; import 'package:flutter/material.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/utils/solid_file_operations.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; @@ -43,14 +44,20 @@ class SolidFileDefaultCallbacks { BuildContext context, String currentPath, GlobalKey browserKey, { + @Deprecated('basePath is no longer used. Paths are relative to Pod root.') String? basePath, }) { + // Normalise paths to ensure consistent handling without leading slashes. + + final normalisedCurrentPath = PathUtils.normalise(currentPath); + return SolidFileUploadCallbacks( onUpload: () { + // Upload using the normalised current path directly. + SolidFileOperations.uploadFile( context, - currentPath, - basePath: basePath, + normalisedCurrentPath, onSuccess: () { // Refresh the file browser after successful upload. diff --git a/lib/src/widgets/solid_file_helpers.dart b/lib/src/widgets/solid_file_helpers.dart index 4e12126..837a552 100644 --- a/lib/src/widgets/solid_file_helpers.dart +++ b/lib/src/widgets/solid_file_helpers.dart @@ -33,6 +33,7 @@ import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'package:solidui/src/models/file_type_config.dart'; +import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; /// Helper class for SolidFile widget utilities. @@ -71,7 +72,12 @@ class SolidFileHelpers { } if (autoConfig && showUpload) { - final typeConfig = FileTypeConfig.fromPath(currentPath, basePath); + // Normalise paths for consistent handling. + + final normalisedCurrentPath = PathUtils.normalise(currentPath); + final normalisedBasePath = PathUtils.normalise(basePath); + final typeConfig = + FileTypeConfig.fromPath(normalisedCurrentPath, normalisedBasePath); return typeConfig.createUploadConfig(); } @@ -92,7 +98,12 @@ class SolidFileHelpers { } if (autoConfig) { - final typeConfig = FileTypeConfig.fromPath(currentPath, basePath); + // Normalise paths for consistent handling. + + final normalisedCurrentPath = PathUtils.normalise(currentPath); + final normalisedBasePath = PathUtils.normalise(basePath); + final typeConfig = + FileTypeConfig.fromPath(normalisedCurrentPath, normalisedBasePath); return typeConfig.displayName; } @@ -102,14 +113,17 @@ class SolidFileHelpers { /// Helper function to get a user-friendly name from the path. static String getFriendlyFolderName(String pathValue, String basePath) { - final String root = basePath; - if (pathValue.isEmpty || pathValue == root) { + // Normalise paths for consistent comparison. + + final normalisedPath = PathUtils.normalise(pathValue); + final normalisedRoot = PathUtils.normalise(basePath); + if (normalisedPath.isEmpty || normalisedPath == normalisedRoot) { return 'Home Folder'; } // Use path.basename to safely get the last component. - final dirName = path.basename(pathValue); + final dirName = path.basename(normalisedPath); switch (dirName) { case 'diary': diff --git a/lib/src/widgets/solid_file_operations.dart b/lib/src/widgets/solid_file_operations.dart index b3c8200..83fc099 100644 --- a/lib/src/widgets/solid_file_operations.dart +++ b/lib/src/widgets/solid_file_operations.dart @@ -39,12 +39,15 @@ import 'package:solidpod/solidpod.dart'; import 'package:solidui/src/models/file_state.dart'; import 'package:solidui/src/utils/is_text_file.dart'; +import 'package:solidui/src/utils/path_utils.dart'; /// Helper class for file operations in SolidFile widget. class SolidFileOperations { /// Handles file upload by reading its contents and encrypting it for upload. + @Deprecated( + 'Use SolidFileUploadOperations.uploadFile with PathType.relativeToPod') static Future handleUpload( FileState fileState, String basePath, @@ -84,13 +87,18 @@ class SolidFileOperations { final remoteFileName = '$sanitizedFileName.enc.ttl'; final cleanFileName = sanitizedFileName; - // Extract the subdirectory path. + // Extract the subdirectory path using PathUtils for consistent + // normalisation (no leading slashes). - String? subPath = - fileState.currentPath?.replaceFirst(basePath, '').trim(); - String uploadPath = subPath == null || subPath.isEmpty + final normalisedBasePath = PathUtils.normalise(basePath); + final normalisedCurrentPath = fileState.currentPath != null + ? PathUtils.normalise(fileState.currentPath!) + : ''; + final subPath = + PathUtils.relativeTo(normalisedCurrentPath, normalisedBasePath); + final uploadPath = subPath.isEmpty ? remoteFileName - : '${subPath.startsWith("/") ? subPath.substring(1) : subPath}/$remoteFileName'; + : PathUtils.combine(subPath, remoteFileName); // Upload file with encryption. @@ -110,6 +118,8 @@ class SolidFileOperations { /// Handles the download and decryption of files from the POD. + @Deprecated( + 'Use SolidFileDownloadOperations.downloadFile with PathType.relativeToPod') static Future handleDownload( FileState fileState, String basePath, @@ -138,10 +148,14 @@ class SolidFileOperations { return fileState.copyWith(downloadInProgress: false); } - final baseDir = basePath; - final relativePath = fileState.currentPath == baseDir - ? '$baseDir/${fileState.remoteFileName}' - : '${fileState.currentPath}/${fileState.remoteFileName}'; + // Use PathUtils for consistent path handling without leading slashes. + + final normalisedBasePath = PathUtils.normalise(basePath); + final normalisedCurrentPath = fileState.currentPath != null + ? PathUtils.normalise(fileState.currentPath!) + : normalisedBasePath; + final relativePath = + PathUtils.combine(normalisedCurrentPath, fileState.remoteFileName!); await promptForKeyFunction(); @@ -168,6 +182,8 @@ class SolidFileOperations { /// Handles file deletion from the POD. + @Deprecated( + 'Use SolidFileDeleteOperations.deletePodFile with PathType.relativeToPod') static Future handleDelete( FileState fileState, String basePath, @@ -182,10 +198,14 @@ class SolidFileOperations { deleteDone: false, ); - final baseDir = basePath; - final filePath = fileState.currentPath == baseDir - ? '$baseDir/${fileState.remoteFileName}' - : '${fileState.currentPath}/${fileState.remoteFileName}'; + // Use PathUtils for consistent path handling without leading slashes. + + final normalisedBasePath = PathUtils.normalise(basePath); + final normalisedCurrentPath = fileState.currentPath != null + ? PathUtils.normalise(fileState.currentPath!) + : normalisedBasePath; + final filePath = + PathUtils.combine(normalisedCurrentPath, fileState.remoteFileName!); // Delete the file (this also handles the ACL file automatically). From 2ccc3bccabfa9933c7cf1a0e21f778563780b835 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 02:24:03 +1100 Subject: [PATCH 04/21] Lint --- lib/src/utils/path_utils.dart | 6 +- lib/src/widgets/security_key_buttons.dart | 96 ++++++++++++ .../widgets/security_key_display_mode.dart | 41 +++++ lib/src/widgets/security_key_header.dart | 111 ++++++++++++++ lib/src/widgets/security_key_ui.dart | 141 +++--------------- lib/src/widgets/solid_file_operations.dart | 9 +- 6 files changed, 278 insertions(+), 126 deletions(-) create mode 100644 lib/src/widgets/security_key_buttons.dart create mode 100644 lib/src/widgets/security_key_display_mode.dart create mode 100644 lib/src/widgets/security_key_header.dart diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart index f936a2a..15eef76 100644 --- a/lib/src/utils/path_utils.dart +++ b/lib/src/utils/path_utils.dart @@ -88,10 +88,8 @@ class PathUtils { /// - join(['', 'myapp', '', 'data']) returns `myapp/data` static String join(List segments) { - final normalisedSegments = segments - .map(normalise) - .where((s) => s.isNotEmpty) - .toList(); + final normalisedSegments = + segments.map(normalise).where((s) => s.isNotEmpty).toList(); return normalisedSegments.join('/'); } diff --git a/lib/src/widgets/security_key_buttons.dart b/lib/src/widgets/security_key_buttons.dart new file mode 100644 index 0000000..69b5788 --- /dev/null +++ b/lib/src/widgets/security_key_buttons.dart @@ -0,0 +1,96 @@ +/// Button row component for the SecurityKeyUI widget. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/src/constants/ui.dart'; + +/// A button row widget for security key UI submit and cancel actions. + +class SecurityKeyButtons extends StatelessWidget { + /// Creates security key action buttons. + + const SecurityKeyButtons({ + required this.canSubmit, + required this.onSubmit, + required this.onCancel, + super.key, + }); + + /// Whether the submit button should be enabled. + + final bool canSubmit; + + /// Callback when the submit button is pressed. + + final VoidCallback onSubmit; + + /// Callback when the cancel button is pressed. + + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + final submitButton = ElevatedButton( + onPressed: canSubmit ? onSubmit : null, + style: ElevatedButton.styleFrom( + backgroundColor: SecurityThemeColors.primary(context), + padding: SecurityLayout.buttonPadding, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SecurityLayout.buttonRadius), + ), + ), + child: const Text( + SecurityStrings.submit, + style: SecurityThemeTextStyles.button, + ), + ); + + final cancelButton = TextButton( + onPressed: onCancel, + style: TextButton.styleFrom( + padding: SecurityLayout.buttonPadding, + ), + child: Text( + SecurityStrings.cancel, + style: SecurityThemeTextStyles.cancelButton(context), + ), + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + cancelButton, + SecurityLayout.horizontalGap, + submitButton, + ], + ); + } +} diff --git a/lib/src/widgets/security_key_display_mode.dart b/lib/src/widgets/security_key_display_mode.dart new file mode 100644 index 0000000..08c64e2 --- /dev/null +++ b/lib/src/widgets/security_key_display_mode.dart @@ -0,0 +1,41 @@ +/// Display mode for the SecurityKeyUI widget. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang + +library; + +/// Display mode for the SecurityKeyUI widget. + +enum SecurityKeyDisplayMode { + /// Display as a fullscreen prompt with a scaffold. + + fullscreen, + + /// Display as an embedded dialog component. + + dialog +} diff --git a/lib/src/widgets/security_key_header.dart b/lib/src/widgets/security_key_header.dart new file mode 100644 index 0000000..d280bf6 --- /dev/null +++ b/lib/src/widgets/security_key_header.dart @@ -0,0 +1,111 @@ +/// Header component for the SecurityKeyUI widget. +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Ashley Tang + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/src/constants/ui.dart'; + +/// A header widget displaying title, WebID, and message for security key UI. + +class SecurityKeyHeader extends StatelessWidget { + /// Creates a security key header. + + const SecurityKeyHeader({ + required this.title, + required this.webId, + required this.message, + super.key, + }); + + /// The title to display. + + final String title; + + /// The WebID to display (null if not logged in). + + final String? webId; + + /// The instructional message to display. + + final String message; + + @override + Widget build(BuildContext context) { + return Padding( + padding: SecurityLayout.contentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title heading. + + Text( + title, + style: SecurityThemeTextStyles.heading(context), + ), + + // Green divider under heading. + + Container( + height: SecurityLayout.dividerHeight, + color: SecurityThemeColors.accent(context), + margin: SecurityLayout.dividerMargin, + ), + + // "Currently logged in as:" label. + + Text( + SecurityStrings.webIdLabel, + style: SecurityThemeTextStyles.label(context), + ), + + // WebID on separate line. + + Padding( + padding: SecurityLayout.webIdPadding, + child: Text( + webId ?? SecurityStrings.notLoggedIn, + style: SecurityThemeTextStyles.webId( + context, + isLoggedIn: webId != null, + ), + ), + ), + + // Instructions text. + + Text( + message, + style: SecurityThemeTextStyles.body(context), + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/security_key_ui.dart b/lib/src/widgets/security_key_ui.dart index e763f79..ff6a0ff 100644 --- a/lib/src/widgets/security_key_ui.dart +++ b/lib/src/widgets/security_key_ui.dart @@ -35,28 +35,16 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:solidui/src/constants/ui.dart'; import 'package:solidui/src/widgets/secret_text_field.dart'; +import 'package:solidui/src/widgets/security_key_buttons.dart'; +import 'package:solidui/src/widgets/security_key_display_mode.dart'; +import 'package:solidui/src/widgets/security_key_header.dart'; import 'package:solidui/src/widgets/solid_login_helper.dart'; import 'package:solidui/src/widgets/solid_theme_notifier.dart'; -/// Display mode for the SecurityKeyUI widget. -/// -/// This enum determines whether the widget should be displayed as a fullscreen prompt -/// or as an embedded dialog component. - -enum SecurityKeyDisplayMode { - /// Display as a fullscreen prompt with a scaffold. - - fullscreen, - - /// Display as an embedded dialog component. - - dialog -} +export 'package:solidui/src/widgets/security_key_display_mode.dart'; -/// A flexible [StatefulWidget] for security key operations with improved UI and WebID display. -/// -/// This widget can be used for both simple security key prompts (single input field) -/// and more complex dialogs (multiple input fields) by providing different configurations. +/// A flexible [StatefulWidget] for security key operations with improved UI and +/// WebID display. class SecurityKeyUI extends StatefulWidget { /// Constructor for the SecurityKeyUI widget. @@ -231,7 +219,11 @@ class _SecurityKeyUIState extends State { children: [ // Header section. - _buildHeader(context), + SecurityKeyHeader( + title: widget.title, + webId: widget.webId, + message: widget.message, + ), // Separator. @@ -251,7 +243,17 @@ class _SecurityKeyUIState extends State { Padding( padding: SecurityLayout.buttonsPadding, - child: _buildButtons(context), + child: SecurityKeyButtons( + canSubmit: _canSubmit, + onSubmit: () async => _submit(context), + onCancel: () { + if (widget.displayMode == SecurityKeyDisplayMode.dialog) { + Navigator.pop(context); + } else { + pushReplacement(context, widget.child); + } + }, + ), ), ], ), @@ -271,60 +273,6 @@ class _SecurityKeyUIState extends State { ); } - /// Builds the header section with title, WebID, and message. - - Widget _buildHeader(BuildContext context) { - return Padding( - padding: SecurityLayout.contentPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title heading. - - Text( - widget.title, - style: SecurityThemeTextStyles.heading(context), - ), - - // Green divider under heading. - - Container( - height: SecurityLayout.dividerHeight, - color: SecurityThemeColors.accent(context), - margin: SecurityLayout.dividerMargin, - ), - - // "Currently logged in as:" label. - - Text( - SecurityStrings.webIdLabel, - style: SecurityThemeTextStyles.label(context), - ), - - // WebID on separate line. - - Padding( - padding: SecurityLayout.webIdPadding, - child: Text( - widget.webId ?? SecurityStrings.notLoggedIn, - style: SecurityThemeTextStyles.webId( - context, - isLoggedIn: widget.webId != null, - ), - ), - ), - - // Instructions text. - - Text( - widget.message, - style: SecurityThemeTextStyles.body(context), - ), - ], - ), - ); - } - /// Builds the form with input fields. Widget _buildForm() { @@ -390,49 +338,4 @@ class _SecurityKeyUIState extends State { ), ); } - - /// Builds the buttons for submit and cancel. - - Widget _buildButtons(BuildContext context) { - final submitButton = ElevatedButton( - onPressed: _canSubmit ? () async => _submit(context) : null, - style: ElevatedButton.styleFrom( - backgroundColor: SecurityThemeColors.primary(context), - padding: SecurityLayout.buttonPadding, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(SecurityLayout.buttonRadius), - ), - ), - child: const Text( - SecurityStrings.submit, - style: SecurityThemeTextStyles.button, - ), - ); - - final cancelButton = TextButton( - onPressed: () { - if (widget.displayMode == SecurityKeyDisplayMode.dialog) { - Navigator.pop(context); - } else { - pushReplacement(context, widget.child); - } - }, - style: TextButton.styleFrom( - padding: SecurityLayout.buttonPadding, - ), - child: Text( - SecurityStrings.cancel, - style: SecurityThemeTextStyles.cancelButton(context), - ), - ); - - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - cancelButton, - SecurityLayout.horizontalGap, - submitButton, - ], - ); - } } diff --git a/lib/src/widgets/solid_file_operations.dart b/lib/src/widgets/solid_file_operations.dart index 83fc099..2bf534b 100644 --- a/lib/src/widgets/solid_file_operations.dart +++ b/lib/src/widgets/solid_file_operations.dart @@ -47,7 +47,8 @@ class SolidFileOperations { /// Handles file upload by reading its contents and encrypting it for upload. @Deprecated( - 'Use SolidFileUploadOperations.uploadFile with PathType.relativeToPod') + 'Use SolidFileUploadOperations.uploadFile with PathType.relativeToPod', + ) static Future handleUpload( FileState fileState, String basePath, @@ -119,7 +120,8 @@ class SolidFileOperations { /// Handles the download and decryption of files from the POD. @Deprecated( - 'Use SolidFileDownloadOperations.downloadFile with PathType.relativeToPod') + 'Use SolidFileDownloadOperations.downloadFile with PathType.relativeToPod', + ) static Future handleDownload( FileState fileState, String basePath, @@ -183,7 +185,8 @@ class SolidFileOperations { /// Handles file deletion from the POD. @Deprecated( - 'Use SolidFileDeleteOperations.deletePodFile with PathType.relativeToPod') + 'Use SolidFileDeleteOperations.deletePodFile with PathType.relativeToPod', + ) static Future handleDelete( FileState fileState, String basePath, From 5a01f4ee7342f00504d4bc078304fc33f8fa3c99 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 16:26:39 +1100 Subject: [PATCH 05/21] Add validation for absolute paths or empty path --- .../utils/solid_file_operations_download.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 5437d34..44cf596 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -52,6 +52,22 @@ class SolidFileDownloadOperations { static Future _isFileInCurrentAppFolder(String filePath) async { try { + // Validate that the file path is a POD-relative path rather than an + // absolute URL or empty string. + + if (filePath.trim().isEmpty) { + debugPrint('Cannot check app folder ownership: file path is empty.'); + return false; + } + + if (filePath.startsWith('http://') || filePath.startsWith('https://')) { + debugPrint( + 'Cannot check app folder ownership: expected a POD-relative ' + 'path but received an absolute URL: $filePath', + ); + return false; + } + final appDataPath = await getDataDirPath(); if (appDataPath.isEmpty) { // If no app data path is available, we cannot determine ownership. From b6e5689e6579bf74cf889e54154f34909befec45 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 22:18:51 +1100 Subject: [PATCH 06/21] Use `pathType: PathType.relativeToPod` instead of `pathType: pathType ?? PathType.relativeToPod` --- lib/src/utils/solid_file_operations.dart | 8 ++------ lib/src/utils/solid_file_operations_download.dart | 10 +++++----- lib/src/widgets/solid_file_browser_builder.dart | 10 ++-------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index ca96c1d..1b4e8df 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -30,8 +30,6 @@ library; import 'package:flutter/material.dart'; -import 'package:solidpod/solidpod.dart'; - import 'package:solidui/src/utils/solid_file_operations_delete.dart'; import 'package:solidui/src/utils/solid_file_operations_download.dart'; import 'package:solidui/src/utils/solid_file_operations_upload.dart'; @@ -46,14 +44,12 @@ class SolidFileOperations { static Future downloadFile( BuildContext context, String fileName, - String filePath, { - PathType? pathType, - }) => + String filePath, + ) => SolidFileDownloadOperations.downloadFile( context, fileName, filePath, - pathType: pathType, ); /// Delete a file from the POD. diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 7408932..240d5af 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -49,9 +49,8 @@ class SolidFileDownloadOperations { static Future downloadFile( BuildContext context, String fileName, - String filePath, { - PathType? pathType, - }) async { + String filePath, + ) async { try { // Let user choose where to save the file. @@ -95,12 +94,13 @@ class SolidFileDownloadOperations { if (!context.mounted) return; - // Read file content from POD. + // Read file content from POD. All paths are relative to the Pod + // root, so we always use PathType.relativeToPod. final normalisedPath = PathUtils.combine(filePath, fileName); final fileContent = await readPod( normalisedPath, - pathType: pathType ?? PathType.relativeToPod, + pathType: PathType.relativeToPod, ); if (!context.mounted) return; diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index ff5f25d..2d11da8 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -30,8 +30,6 @@ library; import 'package:flutter/material.dart'; -import 'package:solidpod/solidpod.dart'; - import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/utils/solid_file_operations.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; @@ -68,20 +66,16 @@ class SolidFileBrowserBuilder { }, onFileDownload: onFileDownload ?? (fileName, filePath) { - // Use PathType.relativeToPod for all paths as they are now - // consistently normalised (no leading slashes). - SolidFileOperations.downloadFile( browserKey.currentContext!, fileName, filePath, - pathType: PathType.relativeToPod, ); }, onFileDelete: onFileDelete ?? (fileName, filePath) { - // filePath is already relative to Pod root (e.g., 'myapp/data/subfolder'). - // No basePath conversion needed as we use PathType.relativeToPod approach. + // filePath is already relative to the Pod root + // (e.g., 'myapp/data/subfolder'). SolidFileOperations.deletePodFile( browserKey.currentContext!, From 3c43e1c09bcb415d4046518108e0a394878ae044 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 23:26:41 +1100 Subject: [PATCH 07/21] Remove basePath in utils folder --- lib/src/utils/path_utils.dart | 5 ----- lib/src/utils/solid_file_operations.dart | 4 ---- lib/src/utils/solid_file_operations_delete.dart | 5 ----- lib/src/utils/solid_file_operations_upload.dart | 2 -- 4 files changed, 16 deletions(-) diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart index 15eef76..12d23c3 100644 --- a/lib/src/utils/path_utils.dart +++ b/lib/src/utils/path_utils.dart @@ -33,11 +33,6 @@ library; /// This class provides methods to normalise and manipulate paths used in /// file browsing operations. All paths are treated as relative to the Pod /// root and should not have leading forward slashes. -/// -/// The `basePath` concept (used in earlier versions of solidpod) has been -/// replaced with consistent use of `PathType.relativeToPod` from solidpod. -/// This ensures that paths like `myapp/data/file.ttl` are correctly resolved -/// without causing double slashes in URLs. class PathUtils { const PathUtils._(); diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index 1b4e8df..4c440be 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -58,8 +58,6 @@ class SolidFileOperations { BuildContext context, String fileName, String filePath, { - @Deprecated('basePath is no longer used. Paths are relative to Pod root.') - String? basePath, VoidCallback? onSuccess, }) => SolidFileDeleteOperations.deletePodFile( @@ -74,8 +72,6 @@ class SolidFileOperations { static Future uploadFile( BuildContext context, String currentPath, { - @Deprecated('basePath is no longer used. Paths are relative to Pod root.') - String? basePath, VoidCallback? onSuccess, }) => SolidFileUploadOperations.uploadFile( diff --git a/lib/src/utils/solid_file_operations_delete.dart b/lib/src/utils/solid_file_operations_delete.dart index 50d20a3..2ea76aa 100644 --- a/lib/src/utils/solid_file_operations_delete.dart +++ b/lib/src/utils/solid_file_operations_delete.dart @@ -43,16 +43,11 @@ class SolidFileDeleteOperations { /// /// The [filePath] should be a directory path relative to the Pod root, /// e.g., `myapp/data` or `myapp/data/subfolder`. - /// - /// Note: The [basePath] parameter is deprecated and will be ignored. - /// File paths are now handled using PathType.relativeToPod instead. static Future deletePodFile( BuildContext context, String fileName, String filePath, { - @Deprecated('basePath is no longer used. Paths are relative to Pod root.') - String? basePath, VoidCallback? onSuccess, }) async { try { diff --git a/lib/src/utils/solid_file_operations_upload.dart b/lib/src/utils/solid_file_operations_upload.dart index b4142a1..d07a179 100644 --- a/lib/src/utils/solid_file_operations_upload.dart +++ b/lib/src/utils/solid_file_operations_upload.dart @@ -50,8 +50,6 @@ class SolidFileUploadOperations { static Future uploadFile( BuildContext context, String currentPath, { - @Deprecated('basePath is no longer used. Paths are relative to Pod root.') - String? basePath, VoidCallback? onSuccess, }) async { try { From fe248899e69ce931f2196d2420f3a7f8b331bfe6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 23:28:09 +1100 Subject: [PATCH 08/21] Remove basePath in widgets folder --- lib/src/widgets/solid_file_callbacks.dart | 6 ++---- lib/src/widgets/solid_file_models.dart | 8 -------- lib/src/widgets/solid_file_path_bar.dart | 5 ----- lib/src/widgets/solid_file_uploader.dart | 2 -- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/src/widgets/solid_file_callbacks.dart b/lib/src/widgets/solid_file_callbacks.dart index 946b97a..10eff9e 100644 --- a/lib/src/widgets/solid_file_callbacks.dart +++ b/lib/src/widgets/solid_file_callbacks.dart @@ -43,10 +43,8 @@ class SolidFileDefaultCallbacks { static SolidFileUploadCallbacks createUploadCallbacks( BuildContext context, String currentPath, - GlobalKey browserKey, { - @Deprecated('basePath is no longer used. Paths are relative to Pod root.') - String? basePath, - }) { + GlobalKey browserKey, + ) { // Normalise paths to ensure consistent handling without leading slashes. final normalisedCurrentPath = PathUtils.normalise(currentPath); diff --git a/lib/src/widgets/solid_file_models.dart b/lib/src/widgets/solid_file_models.dart index 3942e2c..efc0bdb 100644 --- a/lib/src/widgets/solid_file_models.dart +++ b/lib/src/widgets/solid_file_models.dart @@ -35,13 +35,6 @@ import 'package:solidui/src/widgets/solid_file_upload_config.dart'; /// Configuration for the SolidFile widget. class SolidFileConfig { - /// Base path for file operations. - /// - /// If null, defaults to the app data directory path (e.g. 'appname/data'). - /// If the app data directory does not exist, falls back to the pod root. - - final String? basePath; - /// Whether to show the back button. final bool showBackButton; @@ -59,7 +52,6 @@ class SolidFileConfig { final double? browserHeight; const SolidFileConfig({ - this.basePath, this.showBackButton = true, this.backButtonText = 'Back to Home Folder', this.forceWideScreen, diff --git a/lib/src/widgets/solid_file_path_bar.dart b/lib/src/widgets/solid_file_path_bar.dart index b5752ac..11f94ac 100644 --- a/lib/src/widgets/solid_file_path_bar.dart +++ b/lib/src/widgets/solid_file_path_bar.dart @@ -66,10 +66,6 @@ class PathBar extends StatelessWidget { final String friendlyFolderName; - /// Base path of the file browser. - - final String basePath; - const PathBar({ super.key, required this.currentPath, @@ -80,7 +76,6 @@ class PathBar extends StatelessWidget { required this.currentDirFileCount, required this.currentDirDirectoryCount, required this.friendlyFolderName, - required this.basePath, }); @override diff --git a/lib/src/widgets/solid_file_uploader.dart b/lib/src/widgets/solid_file_uploader.dart index 49aba45..8b079b4 100644 --- a/lib/src/widgets/solid_file_uploader.dart +++ b/lib/src/widgets/solid_file_uploader.dart @@ -42,14 +42,12 @@ class SolidFileUploader extends StatefulWidget { final Future Function() onUpload; final void Function(String?) onFileSelected; final void Function(String) onPreviewRequested; - final String basePath; const SolidFileUploader({ super.key, required this.fileState, required this.onUpload, required this.onFileSelected, required this.onPreviewRequested, - required this.basePath, }); @override State createState() => _SolidFileUploaderState(); From 00a4e532563307b3ad6dce53e368bb087b555faf Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 23:34:04 +1100 Subject: [PATCH 09/21] Remove basePath in widgets folder --- lib/src/widgets/solid_file.dart | 42 ++-------------- lib/src/widgets/solid_file_browser.dart | 49 +++++++++++-------- .../widgets/solid_file_browser_builder.dart | 7 --- 3 files changed, 34 insertions(+), 64 deletions(-) diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index 096f3fe..df9ea8c 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -45,13 +45,6 @@ import 'package:solidui/src/widgets/solid_file_upload_config.dart'; /// functionality. class SolidFile extends StatefulWidget { - /// Base path for file operations. - /// - /// If null, defaults to the app data directory path (e.g. 'appname/data'). - /// If the app data directory does not exist, falls back to the pod root (''). - - final String? basePath; - /// Current path in the file browser. final String? currentPath; @@ -132,7 +125,6 @@ class SolidFile extends StatefulWidget { const SolidFile({ super.key, - this.basePath, this.currentPath, this.friendlyFolderName, this.showBackButton = true, @@ -162,8 +154,7 @@ class SolidFile extends StatefulWidget { required SolidFileCallbacks callbacks, required SolidFileState state, this.browserKey, - }) : basePath = config.basePath, - currentPath = state.currentPath, + }) : currentPath = state.currentPath, friendlyFolderName = state.friendlyFolderName, showBackButton = config.showBackButton, backButtonText = config.backButtonText, @@ -182,7 +173,7 @@ class SolidFile extends StatefulWidget { uploadState = state.uploadState, autoConfig = false; // Legacy mode does not use auto-config. - /// Default base path constant representing the pod root. + /// Default path constant representing the POD root. static const String podRoot = ''; @@ -194,7 +185,7 @@ class _SolidFileState extends State { late GlobalKey _browserKey; late String _currentPath; - /// The resolved base path (either from widget or computed default). + /// The resolved base path. String? _resolvedBasePath; @@ -216,28 +207,13 @@ class _SolidFileState extends State { /// Falls back to pod root if the app data directory cannot be determined. Future _resolveBasePath() async { - if (widget.basePath != null) { - // Use the provided basePath directly, normalised. - - _resolvedBasePath = PathUtils.normalise(widget.basePath!); - _currentPath = widget.currentPath != null - ? PathUtils.normalise(widget.currentPath!) - : _resolvedBasePath!; - setState(() { - _isResolvingBasePath = false; - }); - return; - } - - // Attempt to get the app data directory path. - try { final appDataPath = await getDataDirPath(); _resolvedBasePath = PathUtils.normalise(appDataPath); } catch (e) { - // Fall back to pod root if getDataDirPath fails. + // Fall back to POD root if getDataDirPath fails. - debugPrint('Failed to get app data path, falling back to pod root: $e'); + debugPrint('Failed to get app data path, falling back to POD root: $e'); _resolvedBasePath = SolidFile.podRoot; } @@ -257,13 +233,6 @@ class _SolidFileState extends State { void didUpdateWidget(covariant SolidFile oldWidget) { super.didUpdateWidget(oldWidget); - // Handle basePath changes. - - if (oldWidget.basePath != widget.basePath) { - _resolveBasePath(); - return; - } - final oldPath = oldWidget.currentPath ?? _effectiveBasePath; final newPath = widget.currentPath ?? _effectiveBasePath; @@ -415,7 +384,6 @@ class _SolidFileState extends State { Widget _buildFileBrowser() { return SolidFileBrowserBuilder.build( browserKey: _browserKey, - basePath: _effectiveBasePath, friendlyFolderName: SolidFileHelpers.getEffectiveFriendlyFolderName( _currentPath, _effectiveBasePath, diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart index d58ddf4..bc77632 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -73,10 +73,6 @@ class SolidFileBrowser extends StatefulWidget { final String friendlyFolderName; - /// The base path for the file browser. - - final String basePath; - const SolidFileBrowser({ super.key, required this.onFileSelected, @@ -86,7 +82,6 @@ class SolidFileBrowser extends StatefulWidget { required this.onImportCsv, required this.onDirectoryChanged, required this.friendlyFolderName, - required this.basePath, }); @override @@ -136,12 +131,28 @@ class SolidFileBrowserState extends State { bool isLoggedIn = false; + /// The home path resolved from [getDataDirPath]. + + String _homePath = ''; + @override void initState() { super.initState(); - // Normalise the base path to ensure consistent path handling. + _resolveHomePath(); + } + + /// Resolves the home path internally via [getDataDirPath]. + + Future _resolveHomePath() async { + try { + final appDataPath = await getDataDirPath(); + _homePath = PathUtils.normalise(appDataPath); + } catch (e) { + debugPrint('Failed to get app data path, falling back to POD root: $e'); + _homePath = ''; + } - currentPath = PathUtils.normalise(widget.basePath); + currentPath = _homePath; pathHistory = [currentPath]; _checkLoginStatus(); } @@ -271,29 +282,28 @@ class SolidFileBrowserState extends State { /// Navigate to a specific path in the file browser. void navigateToPath(String path) { - // Normalise both the target path and the base path to ensure consistent - // comparison and prevent issues with leading slashes. + // Normalise the target path to ensure consistent comparison and prevent + // issues with leading slashes. final normalisedPath = PathUtils.normalise(path); - final normalisedBasePath = PathUtils.normalise(widget.basePath); setState(() { currentPath = normalisedPath; - if (normalisedPath == normalisedBasePath || normalisedPath.isEmpty) { - pathHistory = [normalisedBasePath]; + if (normalisedPath == _homePath || normalisedPath.isEmpty) { + pathHistory = [_homePath]; } else { if (pathHistory.isEmpty || pathHistory.last != normalisedPath) { - // Check if the path is under the base path. + // Check if the path is under the home path. - if (normalisedBasePath.isEmpty || - normalisedPath.startsWith('$normalisedBasePath/')) { - pathHistory = [normalisedBasePath]; + if (_homePath.isEmpty || + normalisedPath.startsWith('$_homePath/')) { + pathHistory = [_homePath]; final relativePath = - PathUtils.relativeTo(normalisedPath, normalisedBasePath); + PathUtils.relativeTo(normalisedPath, _homePath); if (relativePath.isNotEmpty) { final segments = relativePath.split('/').where((s) => s.isNotEmpty); - var currentBuildPath = normalisedBasePath; + var currentBuildPath = _homePath; for (final segment in segments) { currentBuildPath = PathUtils.combine(currentBuildPath, segment); pathHistory.add(currentBuildPath); @@ -315,7 +325,7 @@ class SolidFileBrowserState extends State { String _getEffectiveFriendlyFolderName() { return SolidFileOperations.getFriendlyFolderName( currentPath, - widget.basePath, + _homePath, ); } @@ -356,7 +366,6 @@ class SolidFileBrowserState extends State { currentDirFileCount: currentDirFileCount, currentDirDirectoryCount: currentDirDirectoryCount, friendlyFolderName: _getEffectiveFriendlyFolderName(), - basePath: widget.basePath, ), if (isLoggedIn) const SizedBox(height: 12), Expanded(child: _buildContent()), diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index 2d11da8..ae7c19e 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -30,7 +30,6 @@ library; import 'package:flutter/material.dart'; -import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/utils/solid_file_operations.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; @@ -42,7 +41,6 @@ class SolidFileBrowserBuilder { static Widget build({ required GlobalKey browserKey, - required String basePath, required String friendlyFolderName, Function(String fileName, String filePath)? onFileSelected, Function(String fileName, String filePath)? onFileDownload, @@ -51,14 +49,9 @@ class SolidFileBrowserBuilder { required Function(String path) onDirectoryChanged, SolidFileUploadCallbacks? uploadCallbacks, }) { - // Normalise the base path to ensure consistent path handling. - - final normalisedBasePath = PathUtils.normalise(basePath); - return SolidFileBrowser( key: browserKey, browserKey: browserKey, - basePath: normalisedBasePath, friendlyFolderName: friendlyFolderName, onFileSelected: onFileSelected ?? (fileName, filePath) { From b96c7bcddba69c571d80babd7ecc4b9799cd8868 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 23:35:15 +1100 Subject: [PATCH 10/21] Lint --- lib/src/widgets/solid_file_browser.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart index bc77632..850e029 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -295,8 +295,7 @@ class SolidFileBrowserState extends State { if (pathHistory.isEmpty || pathHistory.last != normalisedPath) { // Check if the path is under the home path. - if (_homePath.isEmpty || - normalisedPath.startsWith('$_homePath/')) { + if (_homePath.isEmpty || normalisedPath.startsWith('$_homePath/')) { pathHistory = [_homePath]; final relativePath = PathUtils.relativeTo(normalisedPath, _homePath); From 33dba75ea2e5fa63fad2ba62def5c24b0194c2d3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 23:48:17 +1100 Subject: [PATCH 11/21] Ensure the security key is available before writing encrypted data --- lib/src/utils/solid_file_operations_upload.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/utils/solid_file_operations_upload.dart b/lib/src/utils/solid_file_operations_upload.dart index d07a179..003ec18 100644 --- a/lib/src/utils/solid_file_operations_upload.dart +++ b/lib/src/utils/solid_file_operations_upload.dart @@ -39,6 +39,7 @@ import 'package:solidpod/solidpod.dart'; import 'package:solidui/src/utils/is_text_file.dart'; import 'package:solidui/src/utils/path_utils.dart'; +import 'package:solidui/src/utils/solid_pod_helpers.dart'; /// Upload operations for SolidUI widgets. @@ -112,6 +113,15 @@ class SolidFileUploadOperations { if (!context.mounted) return; + // Ensure the security key is available before writing encrypted data. + + await getKeyFromUserIfRequired( + context, + const Text('Please enter your security key to upload the file'), + ); + + if (!context.mounted) return; + // Upload file with encryption using PathType.relativeToPod. await writePod( From 7b57ad9f6ada8c5e4438ac5b7423636fbb93abe6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sat, 7 Feb 2026 00:10:15 +1100 Subject: [PATCH 12/21] Change the input filePath to a file URL --- lib/src/utils/solid_file_operations.dart | 12 ++-- .../utils/solid_file_operations_download.dart | 72 +++++++++---------- .../widgets/solid_file_browser_builder.dart | 17 ++++- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index 27f00ba..c89cad9 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -30,8 +30,6 @@ library; import 'package:flutter/material.dart'; -import 'package:solidpod/solidpod.dart'; - import 'package:solidui/src/utils/solid_file_operations_delete.dart'; import 'package:solidui/src/utils/solid_file_operations_download.dart'; import 'package:solidui/src/utils/solid_file_operations_upload.dart'; @@ -42,18 +40,18 @@ class SolidFileOperations { const SolidFileOperations._(); /// Download a file from the POD to local storage. + /// + /// [fileUrl] must be an absolute URL pointing to the file in the POD. static Future downloadFile( BuildContext context, String fileName, - String filePath, { - PathType? pathType, - }) => + String fileUrl, + ) => SolidFileDownloadOperations.downloadFile( context, fileName, - filePath, - pathType: pathType, + fileUrl, ); /// Delete a file from the POD. diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 44cf596..57c3171 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -43,31 +43,38 @@ import 'package:solidui/src/utils/solid_pod_helpers.dart'; class SolidFileDownloadOperations { const SolidFileDownloadOperations._(); - /// Checks if a file is within the current app's data folder. + /// Checks if a file URL belongs to the current app's data folder. /// - /// Returns `true` if the file path starts with the app's data directory path, - /// indicating that the current app can decrypt this file. - /// Returns `false` if the file is from another app's folder, meaning - /// decryption may fail as the security key is not available. + /// The URL of a file in a Solid POD follows the structure: + /// `https://HOST/POD_NAME/APP_NAME/PATH_TO_RESOURCE`. + /// + /// This method retrieves the current app's data directory URL and checks + /// whether [fileUrl] falls within it. If the file belongs to a different + /// app's folder, decryption may fail as the security key is not available. + /// + /// Returns `true` if the file URL is within the current app's data folder. + /// Returns `false` otherwise. - static Future _isFileInCurrentAppFolder(String filePath) async { + static Future _isFileInCurrentAppFolder(String fileUrl) async { try { - // Validate that the file path is a POD-relative path rather than an - // absolute URL or empty string. - - if (filePath.trim().isEmpty) { - debugPrint('Cannot check app folder ownership: file path is empty.'); + if (fileUrl.trim().isEmpty) { + debugPrint('Cannot check app folder ownership: file URL is empty.'); return false; } - if (filePath.startsWith('http://') || filePath.startsWith('https://')) { + // Validate that the input is an absolute URL. + + final uri = Uri.tryParse(fileUrl); + if (uri == null || !uri.hasScheme) { debugPrint( - 'Cannot check app folder ownership: expected a POD-relative ' - 'path but received an absolute URL: $filePath', + 'Cannot check app folder ownership: expected an absolute URL ' + 'but received: $fileUrl', ); return false; } + // Retrieve the current app's data directory path and build its URL. + final appDataPath = await getDataDirPath(); if (appDataPath.isEmpty) { // If no app data path is available, we cannot determine ownership. @@ -75,12 +82,9 @@ class SolidFileDownloadOperations { return false; } - // Normalise the file path by removing leading slashes for comparison. + final appDataDirUrl = await getDirUrl(appDataPath); - final normalisedFilePath = - filePath.startsWith('/') ? filePath.substring(1) : filePath; - - return normalisedFilePath.startsWith(appDataPath); + return fileUrl.startsWith(appDataDirUrl); } catch (e) { debugPrint('Error checking app folder ownership: $e'); return false; @@ -156,13 +160,17 @@ class SolidFileDownloadOperations { } /// Default file download implementation. + /// + /// [fileUrl] must be an absolute URL pointing to the file in the POD + /// (e.g., `https://host/pod/app/data/file.enc.ttl`). The app name is + /// extracted from this URL to determine whether the file belongs to the + /// current application's data folder. static Future downloadFile( BuildContext context, String fileName, - String filePath, { - PathType? pathType, - }) async { + String fileUrl, + ) async { try { // Check if the file is an encrypted file from another app's folder. // If so, warn the user that decryption may not be possible. @@ -170,7 +178,7 @@ class SolidFileDownloadOperations { final isEncryptedFile = fileName.endsWith('.enc.ttl'); if (isEncryptedFile) { - final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); + final isInCurrentAppFolder = await _isFileInCurrentAppFolder(fileUrl); if (!isInCurrentAppFolder) { if (!context.mounted) return; @@ -227,23 +235,11 @@ class SolidFileDownloadOperations { if (!context.mounted) return; - // Read file content from POD. - - // dc 20260107: the `basePath` is heavily involved in the file-browsing - // codebase, and this leads to a leading forward slash in `filePath`, - // e.g., /myapp/encryption/ind-keys.ttl. - // This format triggers an error when extracting data from the turtle - // content due to double `//` in the subject of triples. - // Below is a temporary workaround but a better solution is needed to - // fully resolve this issue (e.g., refactor the file-browsing code to - // use `PathType` instead of `basePath`). + // Read file content from POD using the absolute URL directly. final fileContent = await readPod( - [ - filePath.startsWith('/') ? filePath.substring(1) : filePath, - fileName, - ].join('/'), - pathType: pathType ?? PathType.relativeToPod, + fileUrl, + pathType: PathType.absoluteUrl, ); if (!context.mounted) return; diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index 6dccc54..aff8f91 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -62,12 +62,23 @@ class SolidFileBrowserBuilder { debugPrint('File selected: $fileName at $filePath'); }, onFileDownload: onFileDownload ?? - (fileName, filePath) { + (fileName, filePath) async { + // Construct the absolute file URL from the POD-relative path + // and file name. The leading slash is stripped to avoid double + // slashes when building the URL. + + final fullPath = [ + filePath.startsWith('/') ? filePath.substring(1) : filePath, + fileName, + ].join('/'); + final fileUrl = await getFileUrl(fullPath); + + if (!browserKey.currentContext!.mounted) return; + SolidFileOperations.downloadFile( browserKey.currentContext!, fileName, - filePath, - pathType: basePath.trim().isEmpty ? PathType.relativeToPod : null, + fileUrl, ); }, onFileDelete: onFileDelete ?? From 60cfc2397bd64113ba2cba72a8a210cdd9c6c284 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sat, 7 Feb 2026 00:22:12 +1100 Subject: [PATCH 13/21] Add an 'All POD Files' page --- example/lib/app_scaffold.dart | 12 +++++ example/lib/screens/all_pod_files_page.dart | 49 +++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 example/lib/screens/all_pod_files_page.dart diff --git a/example/lib/app_scaffold.dart b/example/lib/app_scaffold.dart index 001ba75..bb5aa4e 100644 --- a/example/lib/app_scaffold.dart +++ b/example/lib/app_scaffold.dart @@ -34,6 +34,7 @@ import 'package:solidui/solidui.dart'; import 'constants/app.dart'; import 'home.dart'; +import 'screens/all_pod_files_page.dart'; import 'screens/sample_page.dart'; final _scaffoldController = SolidScaffoldController(); @@ -73,6 +74,17 @@ class AppScaffold extends StatelessWidget { ''', child: SolidFile(), ), + SolidMenuItem( + icon: Icons.storage, + title: 'All POD Files', + tooltip: ''' + + **All POD Files:** Tap here to browse all folders on your POD + from the root. + + ''', + child: AllPodFilesPage(), + ), ], // APP BAR. diff --git a/example/lib/screens/all_pod_files_page.dart b/example/lib/screens/all_pod_files_page.dart new file mode 100644 index 0000000..14d5839 --- /dev/null +++ b/example/lib/screens/all_pod_files_page.dart @@ -0,0 +1,49 @@ +/// All POD Files page - Displays all folders on the POD for testing. +/// +/// 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: Tony Chen + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/solidui.dart'; + +/// A page that browses all folders and files on the POD from the root. + +class AllPodFilesPage extends StatelessWidget { + const AllPodFilesPage({super.key}); + + @override + Widget build(BuildContext context) { + return const SolidFile( + basePath: SolidFile.podRoot, + friendlyFolderName: 'All POD Files', + showBackButton: true, + backButtonText: 'Back to POD Root', + ); + } +} From f54bb662fdc339c3eb4e51502aa329e5ff08ce09 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sat, 7 Feb 2026 00:42:20 +1100 Subject: [PATCH 14/21] Revert filePath --- lib/src/utils/solid_file_operations.dart | 36 +++---- .../utils/solid_file_operations_download.dart | 96 ++++++++++--------- .../widgets/solid_file_browser_builder.dart | 29 ++---- 3 files changed, 78 insertions(+), 83 deletions(-) diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index c89cad9..9d91e02 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -30,6 +30,8 @@ library; import 'package:flutter/material.dart'; +import 'package:solidpod/solidpod.dart'; + import 'package:solidui/src/utils/solid_file_operations_delete.dart'; import 'package:solidui/src/utils/solid_file_operations_download.dart'; import 'package:solidui/src/utils/solid_file_operations_upload.dart'; @@ -40,29 +42,29 @@ class SolidFileOperations { const SolidFileOperations._(); /// Download a file from the POD to local storage. - /// - /// [fileUrl] must be an absolute URL pointing to the file in the POD. static Future downloadFile( - BuildContext context, - String fileName, - String fileUrl, - ) => + BuildContext context, + String fileName, + String filePath, { + PathType? pathType, + }) => SolidFileDownloadOperations.downloadFile( context, fileName, - fileUrl, + filePath, + pathType: pathType, ); /// Delete a file from the POD. static Future deletePodFile( - BuildContext context, - String fileName, - String filePath, { - String? basePath, - VoidCallback? onSuccess, - }) => + BuildContext context, + String fileName, + String filePath, { + String? basePath, + VoidCallback? onSuccess, + }) => SolidFileDeleteOperations.deletePodFile( context, fileName, @@ -74,10 +76,10 @@ class SolidFileOperations { /// Upload a file to the POD. static Future uploadFile( - BuildContext context, - String currentPath, { - VoidCallback? onSuccess, - }) => + BuildContext context, + String currentPath, { + VoidCallback? onSuccess, + }) => SolidFileUploadOperations.uploadFile( context, currentPath, diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 57c3171..852866c 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -43,38 +43,31 @@ import 'package:solidui/src/utils/solid_pod_helpers.dart'; class SolidFileDownloadOperations { const SolidFileDownloadOperations._(); - /// Checks if a file URL belongs to the current app's data folder. + /// Checks if a file is within the current app's data folder. /// - /// The URL of a file in a Solid POD follows the structure: - /// `https://HOST/POD_NAME/APP_NAME/PATH_TO_RESOURCE`. - /// - /// This method retrieves the current app's data directory URL and checks - /// whether [fileUrl] falls within it. If the file belongs to a different - /// app's folder, decryption may fail as the security key is not available. - /// - /// Returns `true` if the file URL is within the current app's data folder. - /// Returns `false` otherwise. + /// Returns `true` if the file path starts with the app's data directory path, + /// indicating that the current app can decrypt this file. + /// Returns `false` if the file is from another app's folder, meaning + /// decryption may fail as the security key is not available. - static Future _isFileInCurrentAppFolder(String fileUrl) async { + static Future _isFileInCurrentAppFolder(String filePath) async { try { - if (fileUrl.trim().isEmpty) { - debugPrint('Cannot check app folder ownership: file URL is empty.'); + // Validate that the file path is a POD-relative path rather than an + // absolute URL or empty string. + + if (filePath.trim().isEmpty) { + debugPrint('Cannot check app folder ownership: file path is empty.'); return false; } - // Validate that the input is an absolute URL. - - final uri = Uri.tryParse(fileUrl); - if (uri == null || !uri.hasScheme) { + if (filePath.startsWith('http://') || filePath.startsWith('https://')) { debugPrint( - 'Cannot check app folder ownership: expected an absolute URL ' - 'but received: $fileUrl', + 'Cannot check app folder ownership: expected a POD-relative ' + 'path but received an absolute URL: $filePath', ); return false; } - // Retrieve the current app's data directory path and build its URL. - final appDataPath = await getDataDirPath(); if (appDataPath.isEmpty) { // If no app data path is available, we cannot determine ownership. @@ -82,9 +75,12 @@ class SolidFileDownloadOperations { return false; } - final appDataDirUrl = await getDirUrl(appDataPath); + // Normalise the file path by removing leading slashes for comparison. + + final normalisedFilePath = + filePath.startsWith('/') ? filePath.substring(1) : filePath; - return fileUrl.startsWith(appDataDirUrl); + return normalisedFilePath.startsWith(appDataPath); } catch (e) { debugPrint('Error checking app folder ownership: $e'); return false; @@ -98,8 +94,8 @@ class SolidFileDownloadOperations { /// Returns `false` if the user cancels. static Future _showCrossAppDecryptionWarning( - BuildContext context, - ) async { + BuildContext context, + ) async { final result = await showDialog( context: context, barrierDismissible: false, @@ -117,20 +113,20 @@ class SolidFileDownloadOperations { children: [ Text( 'This encrypted file belongs to another application\'s data ' - 'folder.', + 'folder.', style: TextStyle(fontWeight: FontWeight.w500), ), SizedBox(height: 12), Text( 'The file browser can browse files across all app folders in ' - 'your POD, but can only decrypt files within the current app\'s ' - 'data folder.', + 'your POD, but can only decrypt files within the current app\'s ' + 'data folder.', ), SizedBox(height: 12), Text( 'Since this file was encrypted by a different application, ' - 'the security key required to decrypt it is not available. ' - 'The downloaded file will likely be unreadable or corrupted.', + 'the security key required to decrypt it is not available. ' + 'The downloaded file will likely be unreadable or corrupted.', ), SizedBox(height: 16), Text( @@ -160,17 +156,13 @@ class SolidFileDownloadOperations { } /// Default file download implementation. - /// - /// [fileUrl] must be an absolute URL pointing to the file in the POD - /// (e.g., `https://host/pod/app/data/file.enc.ttl`). The app name is - /// extracted from this URL to determine whether the file belongs to the - /// current application's data folder. static Future downloadFile( - BuildContext context, - String fileName, - String fileUrl, - ) async { + BuildContext context, + String fileName, + String filePath, { + PathType? pathType, + }) async { try { // Check if the file is an encrypted file from another app's folder. // If so, warn the user that decryption may not be possible. @@ -178,7 +170,7 @@ class SolidFileDownloadOperations { final isEncryptedFile = fileName.endsWith('.enc.ttl'); if (isEncryptedFile) { - final isInCurrentAppFolder = await _isFileInCurrentAppFolder(fileUrl); + final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); if (!isInCurrentAppFolder) { if (!context.mounted) return; @@ -235,11 +227,23 @@ class SolidFileDownloadOperations { if (!context.mounted) return; - // Read file content from POD using the absolute URL directly. + // Read file content from POD. + + // dc 20260107: the `basePath` is heavily involved in the file-browsing + // codebase, and this leads to a leading forward slash in `filePath`, + // e.g., /myapp/encryption/ind-keys.ttl. + // This format triggers an error when extracting data from the turtle + // content due to double `//` in the subject of triples. + // Below is a temporary workaround but a better solution is needed to + // fully resolve this issue (e.g., refactor the file-browsing code to + // use `PathType` instead of `basePath`). final fileContent = await readPod( - fileUrl, - pathType: PathType.absoluteUrl, + [ + filePath.startsWith('/') ? filePath.substring(1) : filePath, + fileName, + ].join('/'), + pathType: pathType ?? PathType.relativeToPod, ); if (!context.mounted) return; @@ -303,9 +307,9 @@ class SolidFileDownloadOperations { /// Save decrypted content to a file. static Future _saveDecryptedContent( - String content, - String outputPath, - ) async { + String content, + String outputPath, + ) async { final file = File(outputPath); try { diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index aff8f91..8caf4ba 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -58,31 +58,20 @@ class SolidFileBrowserBuilder { basePath: basePath, friendlyFolderName: friendlyFolderName, onFileSelected: onFileSelected ?? - (fileName, filePath) { + (fileName, filePath) { debugPrint('File selected: $fileName at $filePath'); }, onFileDownload: onFileDownload ?? - (fileName, filePath) async { - // Construct the absolute file URL from the POD-relative path - // and file name. The leading slash is stripped to avoid double - // slashes when building the URL. - - final fullPath = [ - filePath.startsWith('/') ? filePath.substring(1) : filePath, - fileName, - ].join('/'); - final fileUrl = await getFileUrl(fullPath); - - if (!browserKey.currentContext!.mounted) return; - + (fileName, filePath) { SolidFileOperations.downloadFile( browserKey.currentContext!, fileName, - fileUrl, + filePath, + pathType: basePath.trim().isEmpty ? PathType.relativeToPod : null, ); }, onFileDelete: onFileDelete ?? - (fileName, filePath) { + (fileName, filePath) { SolidFileOperations.deletePodFile( browserKey.currentContext!, fileName, @@ -97,12 +86,12 @@ class SolidFileBrowserBuilder { }, onImportCsv: uploadCallbacks?.onImportCsv != null ? (String fileName, String filePath) { - uploadCallbacks!.onImportCsv!(); - } + uploadCallbacks!.onImportCsv!(); + } : onImportCsv ?? (String fileName, String filePath) { - debugPrint('Import CSV: $fileName at $filePath'); - }, + debugPrint('Import CSV: $fileName at $filePath'); + }, onDirectoryChanged: onDirectoryChanged, ); } From f484dc8f7c79558dadf8dc617005cc4611805a1a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sat, 7 Feb 2026 00:47:54 +1100 Subject: [PATCH 15/21] Revert filePath --- lib/src/utils/solid_file_operations.dart | 30 ++++++++-------- .../utils/solid_file_operations_download.dart | 34 +++++++++---------- .../widgets/solid_file_browser_builder.dart | 14 ++++---- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/src/utils/solid_file_operations.dart b/lib/src/utils/solid_file_operations.dart index 9d91e02..27f00ba 100644 --- a/lib/src/utils/solid_file_operations.dart +++ b/lib/src/utils/solid_file_operations.dart @@ -44,11 +44,11 @@ class SolidFileOperations { /// Download a file from the POD to local storage. static Future downloadFile( - BuildContext context, - String fileName, - String filePath, { - PathType? pathType, - }) => + BuildContext context, + String fileName, + String filePath, { + PathType? pathType, + }) => SolidFileDownloadOperations.downloadFile( context, fileName, @@ -59,12 +59,12 @@ class SolidFileOperations { /// Delete a file from the POD. static Future deletePodFile( - BuildContext context, - String fileName, - String filePath, { - String? basePath, - VoidCallback? onSuccess, - }) => + BuildContext context, + String fileName, + String filePath, { + String? basePath, + VoidCallback? onSuccess, + }) => SolidFileDeleteOperations.deletePodFile( context, fileName, @@ -76,10 +76,10 @@ class SolidFileOperations { /// Upload a file to the POD. static Future uploadFile( - BuildContext context, - String currentPath, { - VoidCallback? onSuccess, - }) => + BuildContext context, + String currentPath, { + VoidCallback? onSuccess, + }) => SolidFileUploadOperations.uploadFile( context, currentPath, diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 852866c..44cf596 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -63,7 +63,7 @@ class SolidFileDownloadOperations { if (filePath.startsWith('http://') || filePath.startsWith('https://')) { debugPrint( 'Cannot check app folder ownership: expected a POD-relative ' - 'path but received an absolute URL: $filePath', + 'path but received an absolute URL: $filePath', ); return false; } @@ -78,7 +78,7 @@ class SolidFileDownloadOperations { // Normalise the file path by removing leading slashes for comparison. final normalisedFilePath = - filePath.startsWith('/') ? filePath.substring(1) : filePath; + filePath.startsWith('/') ? filePath.substring(1) : filePath; return normalisedFilePath.startsWith(appDataPath); } catch (e) { @@ -94,8 +94,8 @@ class SolidFileDownloadOperations { /// Returns `false` if the user cancels. static Future _showCrossAppDecryptionWarning( - BuildContext context, - ) async { + BuildContext context, + ) async { final result = await showDialog( context: context, barrierDismissible: false, @@ -113,20 +113,20 @@ class SolidFileDownloadOperations { children: [ Text( 'This encrypted file belongs to another application\'s data ' - 'folder.', + 'folder.', style: TextStyle(fontWeight: FontWeight.w500), ), SizedBox(height: 12), Text( 'The file browser can browse files across all app folders in ' - 'your POD, but can only decrypt files within the current app\'s ' - 'data folder.', + 'your POD, but can only decrypt files within the current app\'s ' + 'data folder.', ), SizedBox(height: 12), Text( 'Since this file was encrypted by a different application, ' - 'the security key required to decrypt it is not available. ' - 'The downloaded file will likely be unreadable or corrupted.', + 'the security key required to decrypt it is not available. ' + 'The downloaded file will likely be unreadable or corrupted.', ), SizedBox(height: 16), Text( @@ -158,11 +158,11 @@ class SolidFileDownloadOperations { /// Default file download implementation. static Future downloadFile( - BuildContext context, - String fileName, - String filePath, { - PathType? pathType, - }) async { + BuildContext context, + String fileName, + String filePath, { + PathType? pathType, + }) async { try { // Check if the file is an encrypted file from another app's folder. // If so, warn the user that decryption may not be possible. @@ -307,9 +307,9 @@ class SolidFileDownloadOperations { /// Save decrypted content to a file. static Future _saveDecryptedContent( - String content, - String outputPath, - ) async { + String content, + String outputPath, + ) async { final file = File(outputPath); try { diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index 8caf4ba..6dccc54 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -58,11 +58,11 @@ class SolidFileBrowserBuilder { basePath: basePath, friendlyFolderName: friendlyFolderName, onFileSelected: onFileSelected ?? - (fileName, filePath) { + (fileName, filePath) { debugPrint('File selected: $fileName at $filePath'); }, onFileDownload: onFileDownload ?? - (fileName, filePath) { + (fileName, filePath) { SolidFileOperations.downloadFile( browserKey.currentContext!, fileName, @@ -71,7 +71,7 @@ class SolidFileBrowserBuilder { ); }, onFileDelete: onFileDelete ?? - (fileName, filePath) { + (fileName, filePath) { SolidFileOperations.deletePodFile( browserKey.currentContext!, fileName, @@ -86,12 +86,12 @@ class SolidFileBrowserBuilder { }, onImportCsv: uploadCallbacks?.onImportCsv != null ? (String fileName, String filePath) { - uploadCallbacks!.onImportCsv!(); - } + uploadCallbacks!.onImportCsv!(); + } : onImportCsv ?? (String fileName, String filePath) { - debugPrint('Import CSV: $fileName at $filePath'); - }, + debugPrint('Import CSV: $fileName at $filePath'); + }, onDirectoryChanged: onDirectoryChanged, ); } From 0976de1db05b8f4af8021c7b197c5fce49dbb4c7 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sun, 8 Feb 2026 23:32:43 +1100 Subject: [PATCH 16/21] Replace basePath with currentPath --- example/lib/screens/all_pod_files_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/screens/all_pod_files_page.dart b/example/lib/screens/all_pod_files_page.dart index 14d5839..e779af6 100644 --- a/example/lib/screens/all_pod_files_page.dart +++ b/example/lib/screens/all_pod_files_page.dart @@ -40,7 +40,7 @@ class AllPodFilesPage extends StatelessWidget { @override Widget build(BuildContext context) { return const SolidFile( - basePath: SolidFile.podRoot, + currentPath: SolidFile.podRoot, friendlyFolderName: 'All POD Files', showBackButton: true, backButtonText: 'Back to POD Root', From 482db53f94e568993c51a706296ea4aeb50a47bc Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 9 Feb 2026 00:10:48 +1100 Subject: [PATCH 17/21] Refine the code --- .../utils/solid_file_operations_download.dart | 31 ++++++++++++------- lib/src/widgets/solid_file.dart | 5 ++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 3908d16..806c107 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -44,10 +44,10 @@ import 'package:solidui/src/utils/solid_pod_helpers.dart'; class SolidFileDownloadOperations { const SolidFileDownloadOperations._(); - /// Checks if a file is within the current app's data folder. + /// Checks if a file is within the current app's folder on the POD. /// - /// Returns `true` if the file path starts with the app's data directory path, - /// indicating that the current app can decrypt this file. + /// Returns `true` if the file belongs to the current app, indicating that + /// the current app can decrypt this file. /// Returns `false` if the file is from another app's folder, meaning /// decryption may fail as the security key is not available. @@ -69,19 +69,28 @@ class SolidFileDownloadOperations { return false; } + // Resolve the relative file path into a full URL for reliable + // comparison. + + final normalisedPath = PathUtils.normalise(filePath); + final fileUrl = await getFileUrl(normalisedPath); + + // Derive the current app name from getDataDirPath(), which returns + // "APP_NAME/data". The first segment is the app name. + final appDataPath = await getDataDirPath(); - if (appDataPath.isEmpty) { - // If no app data path is available, we cannot determine ownership. + if (appDataPath.isEmpty) return false; - return false; - } + final currentAppName = appDataPath.split('/').first; + if (currentAppName.isEmpty) return false; - // Normalise the file path by removing leading slashes for comparison. + // Build the current app's root directory URL and check whether the + // file URL falls under it. getDirUrl appends a trailing slash, which + // prevents false positives (e.g., "myapp2" matching "myapp"). - final normalisedFilePath = - filePath.startsWith('/') ? filePath.substring(1) : filePath; + final appRootUrl = await getDirUrl(currentAppName); - return normalisedFilePath.startsWith(appDataPath); + return fileUrl.startsWith(appRootUrl); } catch (e) { debugPrint('Error checking app folder ownership: $e'); return false; diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index df9ea8c..08d1674 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -202,9 +202,8 @@ class _SolidFileState extends State { /// Resolves the base path asynchronously. /// - /// If basePath is provided, uses it directly. - /// Otherwise, defaults to the app data directory path. - /// Falls back to pod root if the app data directory cannot be determined. + /// Defaults to the app data directory path via [getDataDirPath]. + /// Falls back to POD root if the app data directory cannot be determined. Future _resolveBasePath() async { try { From cc471ed8c3f13841fe3a880f776b4cd777c2e45d Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 9 Feb 2026 00:36:25 +1100 Subject: [PATCH 18/21] Add initialPath to enable browsing files on the root directory of the POD --- lib/src/widgets/solid_file.dart | 39 ++++++++++++------- lib/src/widgets/solid_file_browser.dart | 35 ++++++++++++++--- .../widgets/solid_file_browser_builder.dart | 2 + 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index 08d1674..4c2e3f4 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -202,23 +202,32 @@ class _SolidFileState extends State { /// Resolves the base path asynchronously. /// - /// Defaults to the app data directory path via [getDataDirPath]. - /// Falls back to POD root if the app data directory cannot be determined. + /// If [widget.currentPath] is explicitly provided, it is used as the base + /// path. This allows the widget to start from any location on the POD, + /// including the root. Otherwise, defaults to the app data directory path + /// via [getDataDirPath]. Falls back to POD root if the app data directory + /// cannot be determined. Future _resolveBasePath() async { - try { - final appDataPath = await getDataDirPath(); - _resolvedBasePath = PathUtils.normalise(appDataPath); - } catch (e) { - // Fall back to POD root if getDataDirPath fails. - - debugPrint('Failed to get app data path, falling back to POD root: $e'); - _resolvedBasePath = SolidFile.podRoot; + if (widget.currentPath != null) { + // Use the explicitly provided path as the base. + + _resolvedBasePath = PathUtils.normalise(widget.currentPath!); + } else { + try { + final appDataPath = await getDataDirPath(); + _resolvedBasePath = PathUtils.normalise(appDataPath); + } catch (e) { + // Fall back to POD root if getDataDirPath fails. + + debugPrint( + 'Failed to get app data path, falling back to POD root: $e', + ); + _resolvedBasePath = SolidFile.podRoot; + } } - _currentPath = widget.currentPath != null - ? PathUtils.normalise(widget.currentPath!) - : _resolvedBasePath!; + _currentPath = _resolvedBasePath!; setState(() { _isResolvingBasePath = false; }); @@ -379,6 +388,9 @@ class _SolidFileState extends State { } /// Builds the file browser widget. + /// + /// Passes the resolved [_currentPath] as the initial path so the browser + /// starts from the correct location (e.g., POD root for "All POD Files"). Widget _buildFileBrowser() { return SolidFileBrowserBuilder.build( @@ -389,6 +401,7 @@ class _SolidFileState extends State { widget.autoConfig, widget.friendlyFolderName, ), + initialPath: widget.currentPath, onFileSelected: widget.onFileSelected, onFileDownload: widget.onFileDownload, onFileDelete: widget.onFileDelete, diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart index 850e029..fd43a5f 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -73,6 +73,14 @@ class SolidFileBrowser extends StatefulWidget { final String friendlyFolderName; + /// Optional initial path for the browser to start from. + /// + /// If provided, the browser will start from this path instead of the + /// default app data directory path. Use an empty string to start from + /// the POD root. + + final String? initialPath; + const SolidFileBrowser({ super.key, required this.onFileSelected, @@ -82,6 +90,7 @@ class SolidFileBrowser extends StatefulWidget { required this.onImportCsv, required this.onDirectoryChanged, required this.friendlyFolderName, + this.initialPath, }); @override @@ -142,14 +151,28 @@ class SolidFileBrowserState extends State { } /// Resolves the home path internally via [getDataDirPath]. + /// + /// If [widget.initialPath] is provided, it is used as both the starting + /// path and the home path for navigation. This allows browsing from any + /// location on the POD, including the root. Future _resolveHomePath() async { - try { - final appDataPath = await getDataDirPath(); - _homePath = PathUtils.normalise(appDataPath); - } catch (e) { - debugPrint('Failed to get app data path, falling back to POD root: $e'); - _homePath = ''; + if (widget.initialPath != null) { + // Use the explicitly provided initial path as both the home path and + // the starting path. This ensures the "back to root" navigation and + // path history work correctly for non-default starting locations. + + _homePath = PathUtils.normalise(widget.initialPath!); + } else { + try { + final appDataPath = await getDataDirPath(); + _homePath = PathUtils.normalise(appDataPath); + } catch (e) { + debugPrint( + 'Failed to get app data path, falling back to POD root: $e', + ); + _homePath = ''; + } } currentPath = _homePath; diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index ae7c19e..539df14 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -42,6 +42,7 @@ class SolidFileBrowserBuilder { static Widget build({ required GlobalKey browserKey, required String friendlyFolderName, + String? initialPath, Function(String fileName, String filePath)? onFileSelected, Function(String fileName, String filePath)? onFileDownload, Function(String fileName, String filePath)? onFileDelete, @@ -53,6 +54,7 @@ class SolidFileBrowserBuilder { key: browserKey, browserKey: browserKey, friendlyFolderName: friendlyFolderName, + initialPath: initialPath, onFileSelected: onFileSelected ?? (fileName, filePath) { debugPrint('File selected: $fileName at $filePath'); From 9f04472a580434ed2adfdde6139970d10488a3c8 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 9 Feb 2026 16:12:56 +1100 Subject: [PATCH 19/21] Adjust the warning message --- .../utils/solid_file_operations_download.dart | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 806c107..dc71410 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -103,7 +103,7 @@ class SolidFileDownloadOperations { /// Returns `true` if the user chooses to proceed with the download. /// Returns `false` if the user cancels. - static Future _showCrossAppDecryptionWarning( + static Future _showCrossAppDownloadWarning( BuildContext context, ) async { final result = await showDialog( @@ -114,7 +114,7 @@ class SolidFileDownloadOperations { children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28), SizedBox(width: 12), - Expanded(child: Text('Decryption Warning')), + Expanded(child: Text('Cross-App Download Warning')), ], ), content: const Column( @@ -122,8 +122,7 @@ class SolidFileDownloadOperations { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'This encrypted file belongs to another application\'s data ' - 'folder.', + 'This file belongs to another application\'s data folder.', style: TextStyle(fontWeight: FontWeight.w500), ), SizedBox(height: 12), @@ -134,9 +133,9 @@ class SolidFileDownloadOperations { ), SizedBox(height: 12), Text( - 'Since this file was encrypted by a different application, ' - 'the security key required to decrypt it is not available. ' - 'The downloaded file will likely be unreadable or corrupted.', + 'The file content may be encrypted by the other application. ' + 'If so, the security key required to decrypt it is not ' + 'available, and the downloaded file might be unreadable.', ), SizedBox(height: 16), Text( @@ -173,22 +172,18 @@ class SolidFileDownloadOperations { String filePath, ) async { try { - // Check if the file is an encrypted file from another app's folder. - // If so, warn the user that decryption may not be possible. + // Check if the file belongs to another app's folder. If so, warn the + // user that the file content may be encrypted. - final isEncryptedFile = fileName.endsWith('.enc.ttl'); + final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); - if (isEncryptedFile) { - final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); - - if (!isInCurrentAppFolder) { - if (!context.mounted) return; + if (!isInCurrentAppFolder) { + if (!context.mounted) return; - final shouldProceed = await _showCrossAppDecryptionWarning(context); + final shouldProceed = await _showCrossAppDownloadWarning(context); - if (!shouldProceed) { - return; - } + if (!shouldProceed) { + return; } } From 07fd2b4d2c0abbf5c362ca70d8d7a057843416c1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 10 Feb 2026 21:12:09 +1100 Subject: [PATCH 20/21] Remove hard-coded path names related to HealthPod --- lib/src/models/data_format_config.dart | 42 ----- lib/src/models/file_type_config.dart | 161 ++++++------------ lib/src/widgets/solid_file.dart | 26 +++ lib/src/widgets/solid_file_browser.dart | 10 ++ .../widgets/solid_file_browser_builder.dart | 2 + lib/src/widgets/solid_file_helpers.dart | 82 +++++---- lib/src/widgets/solid_file_operations.dart | 59 +++---- 7 files changed, 153 insertions(+), 229 deletions(-) diff --git a/lib/src/models/data_format_config.dart b/lib/src/models/data_format_config.dart index a0c53dd..0e3c223 100644 --- a/lib/src/models/data_format_config.dart +++ b/lib/src/models/data_format_config.dart @@ -97,45 +97,3 @@ class DataFormatConfig { description.hashCode; } } - -/// Pre-defined format configurations for common health data types. - -class SolidFileDataFormats { - static const bloodPressure = DataFormatConfig( - title: 'Blood Pressure CSV Format', - requiredFields: ['timestamp', 'systolic', 'diastolic', 'heart_rate'], - optionalFields: ['notes'], - ); - - static const vaccination = DataFormatConfig( - title: 'Vaccination CSV Format', - requiredFields: ['timestamp', 'name', 'type'], - optionalFields: ['location', 'notes', 'batch_number'], - ); - - static const medication = DataFormatConfig( - title: 'Medication CSV Format', - requiredFields: ['timestamp', 'name', 'dosage', 'frequency', 'start_date'], - optionalFields: ['notes'], - ); - - static const diary = DataFormatConfig( - title: 'Appointment CSV Format', - requiredFields: ['timestamp', 'content'], - optionalFields: ['mood', 'tags', 'notes'], - ); - - static const profile = DataFormatConfig( - title: 'Profile JSON Format', - requiredFields: [ - 'name', - 'address', - 'bestContactPhone', - 'alternativeContactNumber', - 'email', - 'dateOfBirth', - 'gender', - ], - isJson: true, - ); -} diff --git a/lib/src/models/file_type_config.dart b/lib/src/models/file_type_config.dart index 3ad21ba..eda4b08 100644 --- a/lib/src/models/file_type_config.dart +++ b/lib/src/models/file_type_config.dart @@ -33,23 +33,25 @@ import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_helpers.dart'; import 'package:solidui/src/widgets/solid_file_upload_config.dart'; -/// Predefined file types for different data categories. - -enum SolidFileType { - bloodPressure, - vaccination, - medication, - diary, - profile, - general, -} +/// A callback type for resolving file type configurations from a path. +/// +/// Applications can provide their own resolver to map directory paths to +/// specific [FileTypeConfig] instances. Return `null` to fall through to the +/// default generic behaviour. + +typedef FileTypeResolver = FileTypeConfig? Function( + String normalisedPath, + String? basePath, +); /// Configuration for different file types. class FileTypeConfig { - /// The file type. + /// A string identifier for this file type (e.g. 'general', 'blood_pressure'). + /// + /// Applications may define their own type identifiers. - final SolidFileType type; + final String typeId; /// Display name for the folder. @@ -84,7 +86,7 @@ class FileTypeConfig { final String uploadTooltip; const FileTypeConfig({ - required this.type, + this.typeId = 'general', required this.displayName, this.showCsvButtons = false, this.showProfileButtons = false, @@ -97,121 +99,58 @@ class FileTypeConfig { /// Gets the file type configuration based on the current path. - static FileTypeConfig fromPath(String currentPath, [String? basePath]) { + static FileTypeConfig fromPath( + String currentPath, [ + String? basePath, + FileTypeResolver? resolver, + ]) { // Normalise the path for consistent pattern matching. final normalisedPath = PathUtils.normalise(currentPath); - if (normalisedPath.contains('/blood_pressure') || - normalisedPath.contains('blood_pressure/') || - normalisedPath.endsWith('blood_pressure')) { - return const FileTypeConfig( - type: SolidFileType.bloodPressure, - displayName: 'Blood Pressure Data', - showCsvButtons: true, - formatConfig: SolidFileDataFormats.bloodPressure, - uploadTooltip: ''' + // Attempt app-specific resolution first. -**Upload**: Tap here to upload a file to your Solid Health Pod. + if (resolver != null) { + final resolved = resolver(normalisedPath, basePath); + if (resolved != null) return resolved; + } -''', - ); - } else if (normalisedPath.contains('/vaccination') || - normalisedPath.contains('vaccination/') || - normalisedPath.endsWith('vaccination')) { - return const FileTypeConfig( - type: SolidFileType.vaccination, - displayName: 'Vaccination Data', - showCsvButtons: true, - formatConfig: SolidFileDataFormats.vaccination, - uploadTooltip: ''' - -**Upload**: Tap here to upload a file to your Solid Health Pod. + // Generic fallback — derive a friendly display name from the path. -''', - ); - } else if (normalisedPath.contains('/medication') || - normalisedPath.contains('medication/') || - normalisedPath.endsWith('medication')) { - return const FileTypeConfig( - type: SolidFileType.medication, - displayName: 'Medication Data', - showCsvButtons: true, - formatConfig: SolidFileDataFormats.medication, - uploadTooltip: ''' - -**Upload**: Tap here to upload a file to your Solid Health Pod. + String effectiveBasePath = + basePath != null ? PathUtils.normalise(basePath) : ''; -''', - ); - } else if (normalisedPath.contains('/diary') || - normalisedPath.contains('diary/') || - normalisedPath.endsWith('diary')) { - return const FileTypeConfig( - type: SolidFileType.diary, - displayName: 'Appointments Data', - showCsvButtons: true, - formatConfig: SolidFileDataFormats.diary, - uploadTooltip: ''' - -**Upload**: Tap here to upload a file to your Solid Health Pod. + if (effectiveBasePath.isEmpty) { + final segments = + normalisedPath.split('/').where((s) => s.isNotEmpty).toList(); -''', - ); - } else if (normalisedPath.contains('/profile') || - normalisedPath.contains('profile/') || - normalisedPath.endsWith('profile')) { - return const FileTypeConfig( - type: SolidFileType.profile, - displayName: 'Profile Data', - showProfileButtons: true, - formatConfig: SolidFileDataFormats.profile, - uploadTooltip: ''' - -**Upload**: Tap here to upload a file to your Solid Health Pod. + // Construct a reasonable base path — typically the first 2 segments + // for most cases. -''', - ); - } else { - // General case - use the existing friendly folder name logic for - // consistency. If basePath is provided, use it; otherwise, construct a - // reasonable default. - - String effectiveBasePath = - basePath != null ? PathUtils.normalise(basePath) : ''; - - if (effectiveBasePath.isEmpty) { - final segments = - normalisedPath.split('/').where((s) => s.isNotEmpty).toList(); - - // Construct a reasonable base path - typically the first 2 segments - // for most cases. - - if (segments.length >= 2) { - effectiveBasePath = '${segments[0]}/${segments[1]}'; - } else if (segments.length == 1) { - effectiveBasePath = segments[0]; - } + if (segments.length >= 2) { + effectiveBasePath = '${segments[0]}/${segments[1]}'; + } else if (segments.length == 1) { + effectiveBasePath = segments[0]; } + } - final friendlyName = SolidFileHelpers.getFriendlyFolderName( - normalisedPath, - effectiveBasePath, - ); + final friendlyName = SolidFileHelpers.getFriendlyFolderName( + normalisedPath, + effectiveBasePath, + ); - String displayName = - friendlyName == 'Home' ? 'Home Folder' : friendlyName; + String displayName = + friendlyName == 'Home' ? 'Home Folder' : friendlyName; - return FileTypeConfig( - type: SolidFileType.general, - displayName: displayName, - uploadTooltip: ''' + return FileTypeConfig( + typeId: 'general', + displayName: displayName, + uploadTooltip: ''' -**Upload**: Tap here to upload a file to your Solid Health Pod. +**Upload**: Tap here to upload a file to your Solid Pod. ''', - ); - } + ); } /// Creates the upload configuration for this file type. diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index 4c2e3f4..bf85f34 100644 --- a/lib/src/widgets/solid_file.dart +++ b/lib/src/widgets/solid_file.dart @@ -32,6 +32,7 @@ import 'package:flutter/material.dart'; import 'package:solidpod/solidpod.dart' show getDataDirPath; +import 'package:solidui/src/models/file_type_config.dart'; import 'package:solidui/src/utils/path_utils.dart'; import 'package:solidui/src/widgets/solid_file_browser.dart'; import 'package:solidui/src/widgets/solid_file_browser_builder.dart'; @@ -123,6 +124,23 @@ class SolidFile extends StatefulWidget { final bool autoConfig; + /// Optional resolver for mapping paths to file type configurations. + /// + /// Applications can supply their own resolver to provide custom upload + /// configurations, display names, and format configs based on the current + /// directory path. When the resolver returns `null`, the generic default + /// behaviour is used. + + final FileTypeResolver? fileTypeResolver; + + /// Optional map of directory basenames to display names. + /// + /// When provided, the file browser uses these overrides to display + /// user-friendly folder names (e.g. `{'blood_pressure': 'Blood Pressure + /// Data'}`). Entries not found in the map fall back to generic formatting. + + final Map? folderNameOverrides; + const SolidFile({ super.key, this.currentPath, @@ -144,6 +162,8 @@ class SolidFile extends StatefulWidget { this.uploadState, this.browserKey, this.autoConfig = true, + this.fileTypeResolver, + this.folderNameOverrides, }); /// Legacy constructor for backward compatibility. @@ -154,6 +174,8 @@ class SolidFile extends StatefulWidget { required SolidFileCallbacks callbacks, required SolidFileState state, this.browserKey, + this.fileTypeResolver, + this.folderNameOverrides, }) : currentPath = state.currentPath, friendlyFolderName = state.friendlyFolderName, showBackButton = config.showBackButton, @@ -355,6 +377,7 @@ class _SolidFileState extends State { widget.autoConfig, widget.showUpload, widget.uploadConfig, + widget.fileTypeResolver, ), uploadCallbacks: _getEffectiveUploadCallbacks(), uploadState: widget.uploadState ?? @@ -372,6 +395,7 @@ class _SolidFileState extends State { widget.autoConfig, widget.showUpload, widget.uploadConfig, + widget.fileTypeResolver, ), uploadCallbacks: _getEffectiveUploadCallbacks(), uploadState: widget.uploadState ?? @@ -400,6 +424,7 @@ class _SolidFileState extends State { _effectiveBasePath, widget.autoConfig, widget.friendlyFolderName, + widget.fileTypeResolver, ), initialPath: widget.currentPath, onFileSelected: widget.onFileSelected, @@ -408,6 +433,7 @@ class _SolidFileState extends State { onImportCsv: widget.onImportCsv, onDirectoryChanged: _handleDirectoryChanged, uploadCallbacks: widget.uploadCallbacks, + folderNameOverrides: widget.folderNameOverrides, ); } } diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart index fd43a5f..e0f937b 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -81,6 +81,14 @@ class SolidFileBrowser extends StatefulWidget { final String? initialPath; + /// Optional map of directory basenames to display names. + /// + /// When provided, these overrides are used to display user-friendly folder + /// names in the path bar. Entries not found in the map fall back to generic + /// formatting. + + final Map? folderNameOverrides; + const SolidFileBrowser({ super.key, required this.onFileSelected, @@ -91,6 +99,7 @@ class SolidFileBrowser extends StatefulWidget { required this.onDirectoryChanged, required this.friendlyFolderName, this.initialPath, + this.folderNameOverrides, }); @override @@ -348,6 +357,7 @@ class SolidFileBrowserState extends State { return SolidFileOperations.getFriendlyFolderName( currentPath, _homePath, + widget.folderNameOverrides, ); } diff --git a/lib/src/widgets/solid_file_browser_builder.dart b/lib/src/widgets/solid_file_browser_builder.dart index 539df14..c1efd35 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -49,12 +49,14 @@ class SolidFileBrowserBuilder { Function(String fileName, String filePath)? onImportCsv, required Function(String path) onDirectoryChanged, SolidFileUploadCallbacks? uploadCallbacks, + Map? folderNameOverrides, }) { return SolidFileBrowser( key: browserKey, browserKey: browserKey, friendlyFolderName: friendlyFolderName, initialPath: initialPath, + folderNameOverrides: folderNameOverrides, onFileSelected: onFileSelected ?? (fileName, filePath) { debugPrint('File selected: $fileName at $filePath'); diff --git a/lib/src/widgets/solid_file_helpers.dart b/lib/src/widgets/solid_file_helpers.dart index 837a552..4182eec 100644 --- a/lib/src/widgets/solid_file_helpers.dart +++ b/lib/src/widgets/solid_file_helpers.dart @@ -65,8 +65,9 @@ class SolidFileHelpers { String basePath, bool autoConfig, bool showUpload, - SolidFileUploadConfig? uploadConfig, - ) { + SolidFileUploadConfig? uploadConfig, [ + FileTypeResolver? fileTypeResolver, + ]) { if (uploadConfig != null) { return uploadConfig; } @@ -76,8 +77,11 @@ class SolidFileHelpers { final normalisedCurrentPath = PathUtils.normalise(currentPath); final normalisedBasePath = PathUtils.normalise(basePath); - final typeConfig = - FileTypeConfig.fromPath(normalisedCurrentPath, normalisedBasePath); + final typeConfig = FileTypeConfig.fromPath( + normalisedCurrentPath, + normalisedBasePath, + fileTypeResolver, + ); return typeConfig.createUploadConfig(); } @@ -91,8 +95,9 @@ class SolidFileHelpers { String currentPath, String basePath, bool autoConfig, - String? friendlyFolderName, - ) { + String? friendlyFolderName, [ + FileTypeResolver? fileTypeResolver, + ]) { if (friendlyFolderName != null) { return friendlyFolderName; } @@ -102,8 +107,11 @@ class SolidFileHelpers { final normalisedCurrentPath = PathUtils.normalise(currentPath); final normalisedBasePath = PathUtils.normalise(basePath); - final typeConfig = - FileTypeConfig.fromPath(normalisedCurrentPath, normalisedBasePath); + final typeConfig = FileTypeConfig.fromPath( + normalisedCurrentPath, + normalisedBasePath, + fileTypeResolver, + ); return typeConfig.displayName; } @@ -112,7 +120,11 @@ class SolidFileHelpers { /// Helper function to get a user-friendly name from the path. - static String getFriendlyFolderName(String pathValue, String basePath) { + static String getFriendlyFolderName( + String pathValue, + String basePath, [ + Map? folderNameOverrides, + ]) { // Normalise paths for consistent comparison. final normalisedPath = PathUtils.normalise(pathValue); @@ -125,39 +137,25 @@ class SolidFileHelpers { final dirName = path.basename(normalisedPath); - switch (dirName) { - case 'diary': - return 'Appointments Data'; - case 'blood_pressure': - return 'Blood Pressure Data'; - case 'medication': - return 'Medication Data'; - case 'vaccination': - return 'Vaccination Data'; - case 'profile': - return 'Profile Data'; - case 'health_plan': - return 'Health Plan Data'; - case 'pathology': - return 'Pathology Data'; - case 'tv_shows': - return 'TV Shows'; - - default: - // Basic formatting for unknown folders: - // capitalise first letter, replace underscores. - - if (dirName.isEmpty) return 'Folder'; - String formattedName = dirName.replaceAll('_', ' ').trim(); - formattedName = formattedName - .split(RegExp(r'\s+')) - .map( - (w) => w.isEmpty - ? w - : '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}', - ) - .join(' '); - return formattedName; + // Check application-specific overrides first. + + if (folderNameOverrides != null && folderNameOverrides.containsKey(dirName)) { + return folderNameOverrides[dirName]!; } + + // Generic formatting for all folders: + // capitalise first letter, replace underscores. + + if (dirName.isEmpty) return 'Folder'; + String formattedName = dirName.replaceAll('_', ' ').trim(); + formattedName = formattedName + .split(RegExp(r'\s+')) + .map( + (w) => w.isEmpty + ? w + : '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}', + ) + .join(' '); + return formattedName; } } diff --git a/lib/src/widgets/solid_file_operations.dart b/lib/src/widgets/solid_file_operations.dart index 153e103..c7b427d 100644 --- a/lib/src/widgets/solid_file_operations.dart +++ b/lib/src/widgets/solid_file_operations.dart @@ -235,7 +235,11 @@ class SolidFileOperations { /// Helper function to get a user-friendly name from the path. - static String getFriendlyFolderName(String pathValue, String basePath) { + static String getFriendlyFolderName( + String pathValue, + String basePath, [ + Map? folderNameOverrides, + ]) { final String root = basePath; if (pathValue.isEmpty || pathValue == root) { return 'Home Folder'; @@ -245,40 +249,27 @@ class SolidFileOperations { final dirName = path.basename(pathValue); - switch (dirName) { - case 'diary': - return 'Appointments Data'; - case 'blood_pressure': - return 'Blood Pressure Data'; - case 'medication': - return 'Medication Data'; - case 'vaccination': - return 'Vaccination Data'; - case 'profile': - return 'Profile Data'; - case 'health_plan': - return 'Health Plan Data'; - case 'pathology': - return 'Pathology Data'; - case 'tv_shows': - return 'TV Shows'; - - default: - // Basic formatting for unknown folders: - // capitalise first letter, replace underscores. - - if (dirName.isEmpty) return 'Folder'; - String formattedName = dirName.replaceAll('_', ' ').trim(); - formattedName = formattedName - .split(RegExp(r'\s+')) - .map( - (w) => w.isEmpty - ? w - : '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}', - ) - .join(' '); - return formattedName; + // Check application-specific overrides first. + + if (folderNameOverrides != null && + folderNameOverrides.containsKey(dirName)) { + return folderNameOverrides[dirName]!; } + + // Generic formatting for all folders: + // capitalise first letter, replace underscores. + + if (dirName.isEmpty) return 'Folder'; + String formattedName = dirName.replaceAll('_', ' ').trim(); + formattedName = formattedName + .split(RegExp(r'\s+')) + .map( + (w) => w.isEmpty + ? w + : '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}', + ) + .join(' '); + return formattedName; } /// Shows an alert dialog with the given message. From 75f41bc6a8a31a3eb3f27e548a2a3f7eac79d025 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 10 Feb 2026 21:51:48 +1100 Subject: [PATCH 21/21] Lint --- lib/src/models/file_type_config.dart | 3 +-- lib/src/widgets/solid_file_helpers.dart | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/models/file_type_config.dart b/lib/src/models/file_type_config.dart index eda4b08..9b91f11 100644 --- a/lib/src/models/file_type_config.dart +++ b/lib/src/models/file_type_config.dart @@ -139,8 +139,7 @@ class FileTypeConfig { effectiveBasePath, ); - String displayName = - friendlyName == 'Home' ? 'Home Folder' : friendlyName; + String displayName = friendlyName == 'Home' ? 'Home Folder' : friendlyName; return FileTypeConfig( typeId: 'general', diff --git a/lib/src/widgets/solid_file_helpers.dart b/lib/src/widgets/solid_file_helpers.dart index 4182eec..992c22e 100644 --- a/lib/src/widgets/solid_file_helpers.dart +++ b/lib/src/widgets/solid_file_helpers.dart @@ -139,7 +139,8 @@ class SolidFileHelpers { // Check application-specific overrides first. - if (folderNameOverrides != null && folderNameOverrides.containsKey(dirName)) { + if (folderNameOverrides != null && + folderNameOverrides.containsKey(dirName)) { return folderNameOverrides[dirName]!; }