From 5cda0848d6653629e8fc0c4a0b86a995aecf6e6e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 5 Feb 2026 21:15:17 +1100 Subject: [PATCH 01/13] =?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 5a01f4ee7342f00504d4bc078304fc33f8fa3c99 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Fri, 6 Feb 2026 16:26:39 +1100 Subject: [PATCH 02/13] 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 7b57ad9f6ada8c5e4438ac5b7423636fbb93abe6 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sat, 7 Feb 2026 00:10:15 +1100 Subject: [PATCH 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 c0868d1ebde51867599f3f06bc1499637df852b6 Mon Sep 17 00:00:00 2001 From: Graham Williams Date: Tue, 10 Feb 2026 10:45:07 +1100 Subject: [PATCH 11/13] Update templates including loc lint. --- .githooks/pre-commit | 24 ++++++++++++++++++++++++ .githooks/pre-push | 7 ------- .github/workflows/ci.yaml | 2 +- support/loc.sh | 20 +++++++++++--------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 28cc404..08a1cf1 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,3 +15,27 @@ fi echo "$result" printf "\e[32;1m%s\e[0m\n" 'Finished running dart format.' + +# Get the commit message. + +COMMIT_MSG=$(git log -1 --pretty=%B) + +# 20260123 gjw Check if the commit message starts with 'Bump version' +# If it does then run `make versions` to update the version in any +# files that require updating, based on the version from pubspec.yaml. +# Then add any updated files to the commit. I add specific files +# rather than using `.` just to avoid possibly adding things I was not +# planning to. This way we always update, for example, the snap +# version when we 'Bump version', avoiding missing this step, so the +# installer builds that 'Bump version' triggers will get the correct +# versions. + +if [[ $COMMIT_MSG == Bump\ version* ]]; then + printf "\e[33;1m%s\e[0m\n" 'Update versions.' + + make versions + git add snap/snapcraft.yaml + + printf "\e[32;1m%s\e[0m\n" 'Finished updating versions.' + +fi diff --git a/.githooks/pre-push b/.githooks/pre-push index 4e3cc79..9af45a8 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -7,13 +7,6 @@ # # If this script exits with a non-zero status nothing will be pushed. -PVER=$(egrep '^version:' pubspec.yaml | cut -d' ' -f2 | cut -d'+' -f1) - -if [ -d snap ]; then - printf "\e[33;1m%s\e[0m\n" 'Update snap version.' - perl -pi -e "s|^version:.*|version: ${PVER}|" snap/snapcraft.yaml; -fi - # Flutter Analyze printf "\e[33;1m%s\e[0m\n" 'Running flutter analyze.' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index df918bc..731e4e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ on: types: [opened, reopened, synchronize] env: - FLUTTER_VERSION: '3.38.5' + FLUTTER_VERSION: '3.38.9' jobs: diff --git a/support/loc.sh b/support/loc.sh index 124a229..c526c42 100644 --- a/support/loc.sh +++ b/support/loc.sh @@ -23,15 +23,17 @@ show_help() { # Function to cleanse lines. cleanse_lines() { - # Remove: - # empty lines - # comment only lines - # import/library/@override - # lines that start with }, ) or ] - # lines that consist of '? [' or ': [' - # begins a parameter list, like ` names: [` - # - awk 'NF && !/^\s*\/\/|^\s*(import|library|@override)|^\s*[})\]]|^\s*(\?|:) \[|\s*\w*: \[/' "$1" + sed '1d' "$1" | + grep -v '^$' | # Remove empty lines + grep -v '^[[:space:]]*//' | # Remove comment only lines + grep -v '^\(import\|library\)' | # Remove library and import statements + grep -v '^\s*@' | # Remove directives + grep -v '^\s*[\}\)]' | # Remove linest that start with a bracket + grep -v '^\s*\]' | # Needed this as special case + grep -v '^\s*(\?|:) \[' | # Remove lines that consist of '? [' or ': [' + grep -v '\s*\w*: \[' | # Remove parameter list lines like ` names: [` + grep -v "^\s*['][^']*[']" | # Remove lines that are only a string. + cat } wc_cleanse_lines() { From 07fd2b4d2c0abbf5c362ca70d8d7a057843416c1 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 10 Feb 2026 21:12:09 +1100 Subject: [PATCH 12/13] 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 13/13] 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]!; }