Skip to content
Open
36 changes: 26 additions & 10 deletions lib/src/models/file_type_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
library;

import 'package:solidui/src/models/data_format_config.dart';
import 'package:solidui/src/utils/path_utils.dart';
import 'package:solidui/src/widgets/solid_file_helpers.dart';
import 'package:solidui/src/widgets/solid_file_upload_config.dart';

Expand Down Expand Up @@ -97,7 +98,13 @@ class FileTypeConfig {
/// Gets the file type configuration based on the current path.

static FileTypeConfig fromPath(String currentPath, [String? basePath]) {
if (currentPath.contains('/blood_pressure')) {
// Normalise the path for consistent pattern matching.

final normalisedPath = PathUtils.normalise(currentPath);

if (normalisedPath.contains('/blood_pressure') ||
normalisedPath.contains('blood_pressure/') ||
normalisedPath.endsWith('blood_pressure')) {
return const FileTypeConfig(
type: SolidFileType.bloodPressure,
displayName: 'Blood Pressure Data',
Expand All @@ -109,7 +116,9 @@ class FileTypeConfig {

''',
);
} else if (currentPath.contains('/vaccination')) {
} else if (normalisedPath.contains('/vaccination') ||
normalisedPath.contains('vaccination/') ||
normalisedPath.endsWith('vaccination')) {
return const FileTypeConfig(
type: SolidFileType.vaccination,
displayName: 'Vaccination Data',
Expand All @@ -121,7 +130,9 @@ class FileTypeConfig {

''',
);
} else if (currentPath.contains('/medication')) {
} else if (normalisedPath.contains('/medication') ||
normalisedPath.contains('medication/') ||
normalisedPath.endsWith('medication')) {
return const FileTypeConfig(
type: SolidFileType.medication,
displayName: 'Medication Data',
Expand All @@ -133,7 +144,9 @@ class FileTypeConfig {

''',
);
} else if (currentPath.contains('/diary')) {
} else if (normalisedPath.contains('/diary') ||
normalisedPath.contains('diary/') ||
normalisedPath.endsWith('diary')) {
return const FileTypeConfig(
type: SolidFileType.diary,
displayName: 'Appointments Data',
Expand All @@ -145,7 +158,9 @@ class FileTypeConfig {

''',
);
} else if (currentPath.contains('/profile')) {
} else if (normalisedPath.contains('/profile') ||
normalisedPath.contains('profile/') ||
normalisedPath.endsWith('profile')) {
return const FileTypeConfig(
type: SolidFileType.profile,
displayName: 'Profile Data',
Expand All @@ -162,24 +177,25 @@ class FileTypeConfig {
// consistency. If basePath is provided, use it; otherwise, construct a
// reasonable default.

String effectiveBasePath = basePath ?? '';
String effectiveBasePath =
basePath != null ? PathUtils.normalise(basePath) : '';

if (effectiveBasePath.isEmpty) {
final segments =
currentPath.split('/').where((s) => s.isNotEmpty).toList();
normalisedPath.split('/').where((s) => s.isNotEmpty).toList();

// Construct a reasonable base path - typically the first 2 segments
// for most cases.

if (segments.length >= 2) {
effectiveBasePath = '/${segments[0]}/${segments[1]}';
effectiveBasePath = '${segments[0]}/${segments[1]}';
} else if (segments.length == 1) {
effectiveBasePath = '/${segments[0]}';
effectiveBasePath = segments[0];
}
}

final friendlyName = SolidFileHelpers.getFriendlyFolderName(
currentPath,
normalisedPath,
effectiveBasePath,
);

Expand Down
181 changes: 181 additions & 0 deletions lib/src/utils/path_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/// Path utilities for SolidUI.
///
/// Copyright (C) 2026, Software Innovation Institute, ANU.
///
/// Licensed under the MIT License (the "License").
///
/// License: https://choosealicense.com/licenses/mit/.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
///
/// Authors: Tony Chen

library;

/// Utility class for path operations in SolidUI.
///
/// This class provides methods to normalise and manipulate paths used in
/// file browsing operations. All paths are treated as relative to the Pod
/// root and should not have leading forward slashes.

class PathUtils {
const PathUtils._();

/// Normalises a relative path by removing leading and trailing slashes.
///
/// This ensures paths are consistently formatted for use with solidpod's
/// `PathType.relativeToPod` option, which expects paths without leading
/// slashes.
///
/// Examples:
/// - `/myapp/data` becomes `myapp/data`
/// - `myapp/data/` becomes `myapp/data`
/// - `//myapp//data//` becomes `myapp/data`
/// - `` remains ``
/// - `/` becomes ``

static String normalise(String path) {
if (path.isEmpty) return '';

// Remove leading slashes.

String normalised = path;
while (normalised.startsWith('/')) {
normalised = normalised.substring(1);
}

// Remove trailing slashes.

while (normalised.endsWith('/')) {
normalised = normalised.substring(0, normalised.length - 1);
}

// Remove consecutive slashes.

normalised = normalised.replaceAll(RegExp(r'/+'), '/');

return normalised;
}

/// Joins path segments into a normalised path.
///
/// All segments are normalised and empty segments are filtered out.
///
/// Examples:
/// - join(['myapp', 'data', 'file.ttl']) returns `myapp/data/file.ttl`
/// - join(['/myapp/', '/data/', 'file.ttl']) returns `myapp/data/file.ttl`
/// - join(['', 'myapp', '', 'data']) returns `myapp/data`

static String join(List<String> segments) {
final normalisedSegments =
segments.map(normalise).where((s) => s.isNotEmpty).toList();

return normalisedSegments.join('/');
}

/// Extracts the relative path from a full path given a root path.
///
/// Both paths are normalised before comparison. If the full path does not
/// start with the root path, the full normalised path is returned.
///
/// Examples:
/// - relativeTo('myapp/data/subfolder', 'myapp/data') returns `subfolder`
/// - relativeTo('/myapp/data/subfolder', '/myapp/data') returns `subfolder`
/// - relativeTo('myapp/data', 'myapp/data') returns ``
/// - relativeTo('other/path', 'myapp/data') returns `other/path`

static String relativeTo(String fullPath, String rootPath) {
final normalisedFull = normalise(fullPath);
final normalisedRoot = normalise(rootPath);

if (normalisedRoot.isEmpty) {
return normalisedFull;
}

if (normalisedFull == normalisedRoot) {
return '';
}

if (normalisedFull.startsWith('$normalisedRoot/')) {
return normalisedFull.substring(normalisedRoot.length + 1);
}

return normalisedFull;
}

/// Combines a directory path and a file name into a full path.
///
/// Both are normalised before joining.
///
/// Examples:
/// - combine('myapp/data', 'file.ttl') returns `myapp/data/file.ttl`
/// - combine('/myapp/data/', '/file.ttl') returns `myapp/data/file.ttl`
/// - combine('', 'file.ttl') returns `file.ttl`

static String combine(String directoryPath, String fileName) {
return join([directoryPath, fileName]);
}

/// Checks if a path is the root (empty or just slashes).
///
/// Examples:
/// - isRoot('') returns true
/// - isRoot('/') returns true
/// - isRoot('//') returns true
/// - isRoot('myapp') returns false

static bool isRoot(String path) {
return normalise(path).isEmpty;
}

/// Gets the parent directory of a path.
///
/// Returns empty string if the path has no parent (is root or single
/// segment).
///
/// Examples:
/// - parent('myapp/data/subfolder') returns `myapp/data`
/// - parent('myapp') returns ``
/// - parent('') returns ``

static String parent(String path) {
final normalised = normalise(path);
final lastSlash = normalised.lastIndexOf('/');
if (lastSlash == -1) {
return '';
}
return normalised.substring(0, lastSlash);
}

/// Gets the last segment (file or directory name) of a path.
///
/// Examples:
/// - basename('myapp/data/file.ttl') returns `file.ttl`
/// - basename('myapp') returns `myapp`
/// - basename('') returns ``

static String basename(String path) {
final normalised = normalise(path);
final lastSlash = normalised.lastIndexOf('/');
if (lastSlash == -1) {
return normalised;
}
return normalised.substring(lastSlash + 1);
}
}
10 changes: 2 additions & 8 deletions lib/src/utils/solid_file_operations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -46,14 +44,12 @@ class SolidFileOperations {
static Future<void> downloadFile(
BuildContext context,
String fileName,
String filePath, {
PathType? pathType,
}) =>
String filePath,
) =>
SolidFileDownloadOperations.downloadFile(
context,
fileName,
filePath,
pathType: pathType,
);

/// Delete a file from the POD.
Expand All @@ -62,14 +58,12 @@ class SolidFileOperations {
BuildContext context,
String fileName,
String filePath, {
String? basePath,
VoidCallback? onSuccess,
}) =>
SolidFileDeleteOperations.deletePodFile(
context,
fileName,
filePath,
basePath: basePath,
onSuccess: onSuccess,
);

Expand Down
11 changes: 8 additions & 3 deletions lib/src/utils/solid_file_operations_delete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,22 @@ import 'package:flutter/material.dart';

import 'package:solidpod/solidpod.dart';

import 'package:solidui/src/utils/path_utils.dart';

/// Delete operations for SolidUI widgets.

class SolidFileDeleteOperations {
const SolidFileDeleteOperations._();

/// Default file deletion implementation.
///
/// The [filePath] should be a directory path relative to the Pod root,
/// e.g., `myapp/data` or `myapp/data/subfolder`.

static Future<void> deletePodFile(
BuildContext context,
String fileName,
String filePath, {
String? basePath,
VoidCallback? onSuccess,
}) async {
try {
Expand Down Expand Up @@ -92,9 +96,10 @@ class SolidFileDeleteOperations {

try {
// Construct the full file path by combining directory path and
// filename.
// filename. Use PathUtils to ensure no leading slashes, which would
// cause double slashes in the generated URL.

final fullFilePath = [filePath, fileName].join('/');
final fullFilePath = PathUtils.combine(filePath, fileName);

// Delete the file (this also handles the ACL file automatically).

Expand Down
Loading