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/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..e779af6 --- /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( + currentPath: SolidFile.podRoot, + friendlyFolderName: 'All POD Files', + showBackButton: true, + backButtonText: 'Back to POD Root', + ); + } +} 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..9b91f11 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,57 @@ 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/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index 240d5af..dc71410 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -44,6 +44,126 @@ import 'package:solidui/src/utils/solid_pod_helpers.dart'; class SolidFileDownloadOperations { const SolidFileDownloadOperations._(); + /// Checks if a file is within the current app's folder on the POD. + /// + /// 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. + + 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; + } + + // 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) return false; + + final currentAppName = appDataPath.split('/').first; + if (currentAppName.isEmpty) return false; + + // 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 appRootUrl = await getDirUrl(currentAppName); + + return fileUrl.startsWith(appRootUrl); + } 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 _showCrossAppDownloadWarning( + 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('Cross-App Download Warning')), + ], + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'This 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( + '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( + '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 +172,23 @@ class SolidFileDownloadOperations { String filePath, ) async { try { + // Check if the file belongs to another app's folder. If so, warn the + // user that the file content may be encrypted. + + final isInCurrentAppFolder = await _isFileInCurrentAppFolder(filePath); + + if (!isInCurrentAppFolder) { + if (!context.mounted) return; + + final shouldProceed = await _showCrossAppDownloadWarning(context); + + if (!shouldProceed) { + return; + } + } + + if (!context.mounted) return; + // Let user choose where to save the file. final cleanFileName = fileName.replaceAll('.enc.ttl', ''); diff --git a/lib/src/widgets/solid_file.dart b/lib/src/widgets/solid_file.dart index df9ea8c..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, @@ -202,24 +224,32 @@ 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. + /// 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; }); @@ -347,6 +377,7 @@ class _SolidFileState extends State { widget.autoConfig, widget.showUpload, widget.uploadConfig, + widget.fileTypeResolver, ), uploadCallbacks: _getEffectiveUploadCallbacks(), uploadState: widget.uploadState ?? @@ -364,6 +395,7 @@ class _SolidFileState extends State { widget.autoConfig, widget.showUpload, widget.uploadConfig, + widget.fileTypeResolver, ), uploadCallbacks: _getEffectiveUploadCallbacks(), uploadState: widget.uploadState ?? @@ -380,6 +412,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,13 +424,16 @@ class _SolidFileState extends State { _effectiveBasePath, widget.autoConfig, widget.friendlyFolderName, + widget.fileTypeResolver, ), + initialPath: widget.currentPath, onFileSelected: widget.onFileSelected, onFileDownload: widget.onFileDownload, onFileDelete: widget.onFileDelete, 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 850e029..e0f937b 100644 --- a/lib/src/widgets/solid_file_browser.dart +++ b/lib/src/widgets/solid_file_browser.dart @@ -73,6 +73,22 @@ 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; + + /// 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, @@ -82,6 +98,8 @@ class SolidFileBrowser extends StatefulWidget { required this.onImportCsv, required this.onDirectoryChanged, required this.friendlyFolderName, + this.initialPath, + this.folderNameOverrides, }); @override @@ -142,14 +160,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; @@ -325,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 ae7c19e..c1efd35 100644 --- a/lib/src/widgets/solid_file_browser_builder.dart +++ b/lib/src/widgets/solid_file_browser_builder.dart @@ -42,17 +42,21 @@ 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, 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..992c22e 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,26 @@ 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. 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() {