diff --git a/CHANGELOG.md b/CHANGELOG.md index f35311c5..2b466990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The package is available from ## 1.1 Consolidate Android Login ++ Update dependency and use file_picker v12.x [1.0.37] + Updated list of known solid servers [1.0.35 20260902 gjw] + Streamline login logout [1.0.34 20260828 anushkavidanage] + Session storage for web security key [1.0.33 20260819 gjw] diff --git a/example/lib/features/file_service.dart b/example/lib/features/file_service.dart index a26fca18..5c281a95 100644 --- a/example/lib/features/file_service.dart +++ b/example/lib/features/file_service.dart @@ -26,6 +26,7 @@ library; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart' as path; import 'package:solidpod/solidpod.dart'; import 'package:solidui/solidui.dart' show SolidScaffold; @@ -167,10 +168,10 @@ class _FileServiceState extends State { Widget build(BuildContext context) { final browseButton = ElevatedButton( onPressed: () async { - final result = await FilePicker.pickFiles(); - if (result != null) { + final file = await FilePicker.pickFile(); + if (file != null) { setState(() { - uploadFile = result.files.single.path!; + uploadFile = file.path; uploadDone = false; uploadPercent = 0.0; }); @@ -234,10 +235,10 @@ class _FileServiceState extends State { final browseSharedButton = ElevatedButton( onPressed: () async { - final result = await FilePicker.pickFiles(); - if (result != null) { + final file = await FilePicker.pickFile(); + if (file != null) { setState(() { - uploadSharedFile = result.files.single.path!; + uploadSharedFile = file.path; uploadSharedDone = false; uploadSharedPercent = 0.0; }); @@ -315,17 +316,17 @@ class _FileServiceState extends State { deleteInProgress) ? null : () async { - String? outputFile = await FilePicker.saveFile( - dialogTitle: 'Please set the output file:', - // fileName: 'download.bin', + final dirPath = await FilePicker.getDirectoryPath( + dialogTitle: 'Choose a directory to save file', ); - if (outputFile == null) { + if (dirPath == null) { // User canceled the picker debugPrint('Download is cancelled'); } else { + final localFilePath = path.join(dirPath, getRemoteFileName()); setState(() { - downloadFile = outputFile; + downloadFile = localFilePath; }); try { // remoteFileUrl ??= await getRemoteFileUrl(); @@ -335,7 +336,7 @@ class _FileServiceState extends State { await readLargeFile( remoteFilePath: getRemoteFileName(), - localFilePath: outputFile, + localFilePath: localFilePath, onProgress: (received, total) { setState(() { downloadDone = received == total; @@ -375,21 +376,17 @@ class _FileServiceState extends State { deleteInProgress) ? null : () async { - String? outputFile = await FilePicker.saveFile( - dialogTitle: 'Please set the output file:', + final dirPath = await FilePicker.getDirectoryPath( + dialogTitle: 'Choose a directory to save file', ); - if (outputFile == null) { + if (dirPath == null) { // User canceled the picker debugPrint('Download is cancelled'); } else { setState(() { - downloadSharedFile = outputFile; + downloadSharedInProgress = true; }); try { - setState(() { - downloadSharedInProgress = true; - }); - final sharedFileUrl = sharedUrlController.text.trim(); if (sharedFileUrl.isEmpty) { final msg = 'Shared file URL is empty'; @@ -411,10 +408,16 @@ class _FileServiceState extends State { .getRange(3, uri.pathSegments.length) .join('/'); + final localFilePath = path.join(dirPath, fileName); + + setState(() { + downloadSharedFile = localFilePath; + }); + if (context.mounted) { await readLargeFile( remoteFilePath: fileName, - localFilePath: outputFile, + localFilePath: localFilePath, ownerWebId: ownerWebId, onProgress: (received, total) { setState(() { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f210c99e..bd69e24f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,17 +8,18 @@ environment: dependencies: editable: ^2.0.0 - file_picker: ^11.0.2 + file_picker: ^12.1.3 flutter: sdk: flutter - intl: ^0.20.2 + intl: ^0.20.3 markdown_tooltip: ^0.0.10 - rdflib: ^0.2.12 - solidpod: ^1.0.17 + path: ^1.9.1 + rdflib: ^0.2.13 + solidpod: ^1.0.19 solidui: path: .. universal_io: ^2.3.1 - window_manager: ^0.5.1 + window_manager: ^0.5.2 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/lib/src/screens/initial_setup_widgets/enc_key_input_form.dart b/lib/src/screens/initial_setup_widgets/enc_key_input_form.dart index 1820163e..692ca2f4 100644 --- a/lib/src/screens/initial_setup_widgets/enc_key_input_form.dart +++ b/lib/src/screens/initial_setup_widgets/enc_key_input_form.dart @@ -30,11 +30,10 @@ library; -import 'package:flutter/material.dart'; - import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:markdown_tooltip/markdown_tooltip.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:solidui/src/constants/initial_setup.dart'; diff --git a/lib/src/utils/solid_file_operations_download.dart b/lib/src/utils/solid_file_operations_download.dart index c83f0780..9019d8ca 100644 --- a/lib/src/utils/solid_file_operations_download.dart +++ b/lib/src/utils/solid_file_operations_download.dart @@ -29,7 +29,6 @@ library; import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; @@ -159,18 +158,6 @@ class SolidFileDownloadOperations { if (!context.mounted) return; - // Let user choose where to save the file. - - final cleanFileName = fileName.replaceAll('.enc.ttl', ''); - String? outputFile = await FilePicker.saveFile( - dialogTitle: 'Save file as:', - fileName: cleanFileName, - ); - - if (outputFile == null) return; - - if (!context.mounted) return; - // Show loading dialog via a controller, so it can always be torn // down in the `finally` block — even if the originating context // becomes unmounted while the download is in flight. @@ -209,8 +196,16 @@ class SolidFileDownloadOperations { } // Save decrypted content to file. + // Let user choose where to save the file. - await _saveDecryptedContent(fileContent, outputFile); + final cleanFileName = fileName.replaceAll('.enc.ttl', ''); + final outputFileUri = await FilePicker.saveFile( + dialogTitle: 'Save file as:', + fileName: cleanFileName, + bytes: utf8.encode(fileContent), + ); + + if (outputFileUri == null) return; if (!context.mounted) return; @@ -218,7 +213,8 @@ class SolidFileDownloadOperations { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('File downloaded successfully to $outputFile'), + content: + Text('File downloaded successfully to ${outputFileUri.path}'), backgroundColor: Colors.green, duration: const Duration(seconds: 3), ), @@ -254,23 +250,23 @@ class SolidFileDownloadOperations { /// Save decrypted content to a file. - static Future _saveDecryptedContent( - String content, - String outputPath, - ) async { - final file = File(outputPath); + // static Future _saveDecryptedContent( + // String content, + // String outputPath, + // ) async { + // final file = File(outputPath); - try { - // Try to decode as base64 (for binary files). + // try { + // // Try to decode as base64 (for binary files). - final bytes = base64Decode(content); - await file.writeAsBytes(bytes); - } catch (e) { - // If base64 decode fails, treat as text content. + // final bytes = base64Decode(content); + // await file.writeAsBytes(bytes); + // } catch (e) { + // // If base64 decode fails, treat as text content. - await file.writeAsString(content); - } - } + // await file.writeAsString(content); + // } + // } /// Download a mixed batch of files and/or directories from the POD as a /// single zip archive. @@ -313,17 +309,6 @@ class SolidFileDownloadOperations { if (!context.mounted) return; - // Let the user choose where to save the zip file. - - final outputFile = await FilePicker.saveFile( - dialogTitle: 'Save zip as:', - fileName: zipFileName, - ); - - if (outputFile == null) return; - - if (!context.mounted) return; - // Get security key if required. await getKeyFromUserIfRequired( @@ -392,8 +377,15 @@ class SolidFileDownloadOperations { } // Write zip to the selected output path. + // Let the user choose where to save the zip file. + + final outputFileUri = await FilePicker.saveFile( + dialogTitle: 'Save zip as:', + fileName: zipFileName, + bytes: result.zipBytes, + ); - await File(outputFile).writeAsBytes(result.zipBytes); + if (outputFileUri == null) return; if (!context.mounted) return; @@ -409,8 +401,8 @@ class SolidFileDownloadOperations { final summary = parts.join(', '); final successMsg = result.failed.isEmpty - ? 'Downloaded $summary to $outputFile' - : 'Downloaded $summary to $outputFile ' + ? 'Downloaded $summary to ${outputFileUri.path}' + : 'Downloaded $summary to ${outputFileUri.path} ' '(${result.failed.length} could not be read)'; ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/src/utils/solid_file_operations_upload.dart b/lib/src/utils/solid_file_operations_upload.dart index 14135be3..3949c348 100644 --- a/lib/src/utils/solid_file_operations_upload.dart +++ b/lib/src/utils/solid_file_operations_upload.dart @@ -29,7 +29,6 @@ library; import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; @@ -84,14 +83,15 @@ class SolidFileUploadOperations { // one is provided. final result = hasRestriction - ? await FilePicker.pickFiles( + ? await FilePicker.pickFile( type: FileType.custom, allowedExtensions: sanitisedExtensions, ) : await FilePicker.pickFiles(); - if (result == null || result.files.isEmpty) return; + if (result == null) return; + if (result is List && result.isEmpty) return; - final file = result.files.first; + PlatformFile file = result is List ? result.first : result; if (file.path == null) return; // Defensive client-side check in case the underlying picker on a given @@ -129,15 +129,14 @@ class SolidFileUploadOperations { ); try { - final localFile = File(file.path!); String fileContent; // Read file content. + final bytes = await file.readAsBytes(); if (isTextFile(file.path!)) { - fileContent = await localFile.readAsString(); + fileContent = utf8.decode(bytes); } else { - final bytes = await localFile.readAsBytes(); fileContent = base64Encode(bytes); } diff --git a/lib/src/widgets/secret_text_field.dart b/lib/src/widgets/secret_text_field.dart index 26d6dbde..fffbb646 100644 --- a/lib/src/widgets/secret_text_field.dart +++ b/lib/src/widgets/secret_text_field.dart @@ -28,10 +28,9 @@ library; -import 'package:flutter/material.dart'; - import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:solidui/src/constants/ui.dart'; diff --git a/lib/src/widgets/solid_backup_dialog.dart b/lib/src/widgets/solid_backup_dialog.dart index f294135c..68a96eea 100644 --- a/lib/src/widgets/solid_backup_dialog.dart +++ b/lib/src/widgets/solid_backup_dialog.dart @@ -40,7 +40,7 @@ import 'package:gap/gap.dart'; import 'package:markdown_tooltip/markdown_tooltip.dart'; import 'package:solidpod/solidpod.dart' show SecurityKeyVerificationException, isUserLoggedIn; -import 'package:universal_io/io.dart' show File, Platform, exit; +import 'package:universal_io/io.dart' show Platform, exit; import 'package:solidui/src/services/solid_backup_service.dart'; import 'package:solidui/src/widgets/secret_text_field.dart'; @@ -167,18 +167,16 @@ class _SolidBackupDialogState extends State { required Uint8List bytes, required String fileName, }) async { - final path = await FilePicker.saveFile( + final fileUri = await FilePicker.saveFile( dialogTitle: 'Save backup', fileName: fileName, type: FileType.custom, allowedExtensions: const [kBackupFileExtension], bytes: bytes, ); - if (path == null) return null; - if (!kIsWeb) { - await File(path).writeAsBytes(bytes); - } - return path; + if (fileUri == null) return null; + + return fileUri.path; } // Import. @@ -210,22 +208,17 @@ class _SolidBackupDialogState extends State { try { // Pick and read the backup file. - final picked = await FilePicker.pickFiles( + final file = await FilePicker.pickFile( dialogTitle: 'Select a backup file', type: FileType.custom, allowedExtensions: const [kBackupFileExtension], - withData: true, ); - if (picked == null || picked.files.isEmpty) { + if (file == null) { _setImportMessage('Import cancelled.'); return; } - final bytes = picked.files.first.bytes; - if (bytes == null) { - _setImportMessage('Could not read the selected file.', error: true); - return; - } + final bytes = await file.readAsBytes(); // Read the header and confirm the backup belongs to this application // before asking the user for any keys. diff --git a/lib/src/widgets/solid_file_operations.dart b/lib/src/widgets/solid_file_operations.dart index 77fb11f0..bf045ce7 100644 --- a/lib/src/widgets/solid_file_operations.dart +++ b/lib/src/widgets/solid_file_operations.dart @@ -138,18 +138,6 @@ class SolidFileOperations { downloadDone: false, ); - // Let user choose where to save the file. - - String? outputFile = await FilePicker.saveFile( - dialogTitle: 'Save file as:', - fileName: fileState.cleanFileName ?? - fileState.remoteFileName?.replaceAll('.enc.ttl', ''), - ); - - if (outputFile == null) { - return fileState.copyWith(downloadInProgress: false); - } - // Use PathUtils for consistent path handling without leading slashes. final normalisedBasePath = PathUtils.normalise(basePath); @@ -171,9 +159,20 @@ class SolidFileOperations { } // Save the decrypted content to file + // Let user choose where to save the file. + final defaultFileName = 'download'; - final outputFileHandle = File(outputFile); - await outputFileHandle.writeAsString(fileContent); + final outputFileUri = await FilePicker.saveFile( + dialogTitle: 'Save file as:', + fileName: (fileState.cleanFileName ?? + fileState.remoteFileName?.replaceAll('.enc.ttl', '')) ?? + defaultFileName, + bytes: utf8.encode(fileContent), + ); + + if (outputFileUri == null) { + return fileState.copyWith(downloadInProgress: false); + } return newState.copyWith(downloadDone: true, downloadInProgress: false); } catch (e) { diff --git a/lib/src/widgets/solid_file_uploader.dart b/lib/src/widgets/solid_file_uploader.dart index a20061b6..ab9684fd 100644 --- a/lib/src/widgets/solid_file_uploader.dart +++ b/lib/src/widgets/solid_file_uploader.dart @@ -106,15 +106,12 @@ class _SolidFileUploaderState extends State { } Future _handleJsonPreview() async { - final result = await FilePicker.pickFiles( + final file = await FilePicker.pickFile( type: FileType.custom, allowedExtensions: ['json'], ); - if (result != null && result.files.isNotEmpty) { - final file = result.files.first; - if (file.path != null) { - await handlePreview(file.path!); - } + if (file != null && file.path != null) { + await handlePreview(file.path!); } } diff --git a/lib/src/widgets/solid_file_uploader_helpers.dart b/lib/src/widgets/solid_file_uploader_helpers.dart index abdd9e82..39eb8f65 100644 --- a/lib/src/widgets/solid_file_uploader_helpers.dart +++ b/lib/src/widgets/solid_file_uploader_helpers.dart @@ -43,9 +43,9 @@ class SolidFileUploaderHelpers { /// Handles file selection via file picker. static Future pickFile() async { - FilePickerResult? result = await FilePicker.pickFiles(); - if (result != null) { - return result.files.single.path; + final file = await FilePicker.pickFile(); + if (file != null) { + return file.path; } return null; } diff --git a/lib/src/widgets/solid_profile_editor.dart b/lib/src/widgets/solid_profile_editor.dart index 37aaad37..2d327028 100644 --- a/lib/src/widgets/solid_profile_editor.dart +++ b/lib/src/widgets/solid_profile_editor.dart @@ -94,16 +94,13 @@ class _SolidProfileEditorState extends State { // Image picking. Future _pickImage() async { - final result = await FilePicker.pickFiles( + final file = await FilePicker.pickFile( type: FileType.custom, allowedExtensions: ['png', 'jpg', 'jpeg'], - withData: true, ); - if (result == null || result.files.isEmpty) return; + if (file == null) return; - final file = result.files.first; - final bytes = file.bytes; - if (bytes == null) return; + final bytes = await file.readAsBytes(); if (bytes.length > maxProfilePictureBytes) { if (mounted) { diff --git a/pubspec.yaml b/pubspec.yaml index 72b82198..cc6d3275 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: solidui description: 'A UI library for building Solid applications with Flutter.' -version: 1.0.35 +version: 1.0.37 homepage: https://github.com/anusii/solidui # Scaffold a new Solid Pod file-browser app (a pod browser, with navigation @@ -17,7 +17,7 @@ executables: environment: sdk: '>=3.2.3 <4.0.0' - flutter: '>=3.44.0' + flutter: '>=3.47.0' # To automatically upgrade package dependencies to the latest versions: # @@ -30,29 +30,30 @@ environment: dependencies: flutter: sdk: flutter - archive: ^4.0.8 + archive: ^4.0.9 crypto: ^3.0.7 encrypter_plus: ^5.1.0 - file_picker: ^11.0.2 - flutter_form_builder: ^10.3.0+1 - flutter_markdown_plus: ^1.0.7 + file_picker: ^12.1.3 + flutter_form_builder: ^11.0.0 + flutter_markdown_plus: ^1.0.12 form_builder_validators: ^11.3.0 gap: ^3.0.1 - intl: ^0.20.2 - loading_indicator: ^3.1.1 + intl: ^0.20.3 + loading_indicator: ^4.0.2 markdown_tooltip: ^0.0.10 - package_info_plus: ^9.0.0 + package_info_plus: ^10.2.1 path: ^1.9.1 - pdf: ^3.11.3 - printing: ^5.14.2 - rdflib: ^0.2.12 - share_plus: ^12.0.2 - shared_preferences: ^2.5.4 - solidpod: ^1.0.18 + pdf: ^3.13.0 + printing: ^5.15.0 + rdflib: ^0.2.13 + share_plus: ^13.3.0 + shared_preferences: ^2.5.5 + solidpod: ^1.0.19 universal_io: ^2.3.1 url_launcher: ^6.3.2 version_widget: ^1.0.10 - window_manager: ^0.5.1 + window_manager: ^0.5.2 + material_ui: ^1.1.0 dev_dependencies: flutter_lints: ^6.0.0