diff --git a/README.md b/README.md index 41e9e94f1..2be604a50 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ See [`packages/file_picker/README.md`](packages/file_picker/README.md) for the f ## Migration +- **Upgrading to v14?** See the [Migrating to v14 Guide](packages/file_picker/README.md#migrating-to-v14) for the `cross_file` 0.4.0 migration, the raised minimum versions and the removed web reading options. - **Upgrading to v13?** See the [Migrating to v13 Guide](packages/file_picker/README.md#migrating-to-v13) for details on `PlatformFile.length()` returning `Future` and the removal of legacy v12-deprecated parameters. - **Upgrading from pre-v12?** See the [Migrating to v12 Guide](packages/file_picker/README.md#migrating-to-v12) for the federated architecture changes (`FilePicker.pickFiles()` returning `List`, platform options, and streaming). diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b766b9a97..3d03c9bc8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" + sdk: ">=3.11.0 <4.0.0" formatter: page_width: 80 @@ -13,12 +13,12 @@ formatter: dependencies: flutter: sdk: flutter - file_picker: ^13.0.0 - android_file_picker: ^2.0.0 - file_picker_darwin: ^2.0.0 - file_picker_linux: ^2.0.0 - file_picker_web: ^4.0.0 - windows_file_picker: ^2.0.0 + file_picker: ^14.0.0 + android_file_picker: ^3.0.0 + file_picker_darwin: ^3.0.0 + file_picker_linux: ^3.0.0 + file_picker_web: ^5.0.0 + windows_file_picker: ^3.0.0 file: ^7.0.1 dev_dependencies: diff --git a/packages/file_picker/CHANGELOG.md b/packages/file_picker/CHANGELOG.md index f78c44789..6950356b1 100644 --- a/packages/file_picker/CHANGELOG.md +++ b/packages/file_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 14.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. See the [migration guide](https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker#migrating-to-v14). [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Requires Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15. +- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. +- `getDirectoryPath()` and `saveFile()` now take `darwinOptions`, so `DarwinOptions.acceptLabel` also sets their confirm button text on macOS. [#1861](https://github.com/vicajilau/flutter_file_picker/issues/1861) + ## 13.1.0 - Added support for UIScene on iOS. diff --git a/packages/file_picker/README.md b/packages/file_picker/README.md index c2158a5ef..5420a1a96 100644 --- a/packages/file_picker/README.md +++ b/packages/file_picker/README.md @@ -53,7 +53,30 @@ See the [official API reference on pub.dev](https://pub.dev/documentation/file_p ### Darwin implementation notes -The iOS and macOS native implementations live under the shared Darwin source tree (`file_picker_darwin`). The iOS implementation requires iOS 14.0 or newer because it uses `PHPickerViewController` and `PHPickerResult`. +The iOS and macOS native implementations live under the shared Darwin source tree (`file_picker_darwin`). The iOS implementation requires iOS 14.0 or newer because it uses `PHPickerViewController` and `PHPickerResult`. The macOS implementation requires macOS 10.15 or newer. + +## Migrating to v14 + +1. **Minimum Versions Raised**: Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15, required by `cross_file` 0.4.0. + +2. **`PlatformFile.xFile` Uses `cross_file` 0.4.0**: + * `XFile.path`, `XFile.mimeType`, `XFile.saveTo()` and `XFile.fromData` no longer exist, and `XFile.name` is now `Future name()`. + * Use `PlatformFile.path`, `PlatformFile.name`, `readAsBytes()` and `readAsByteStream()` when you do not need an `XFile`. For a file system path, check for a `FileSystemXFile`: + + ```dart + import 'package:cross_file/cross_file.dart'; + + final path = switch (file.xFile) { + FileSystemXFile(:final path) => path, + _ => null, + }; + ``` + * See the [`cross_file` changelog](https://pub.dev/packages/cross_file/changelog) for the full list. + +3. **Web Reading Options Removed**: + * `withData`, `withReadStream` and `readSequential` are removed from `FilePickerWebOptions`. Picked files are no longer read at pick time, and `readAsBytes()`/`readAsByteStream()` read them on demand. + * **v13**: `FilePickerWebOptions(withData: false, withReadStream: true, cancelUploadOnWindowBlur: false)` + * **v14**: `FilePickerWebOptions(cancelUploadOnWindowBlur: false)` ## Migrating to v13 diff --git a/packages/file_picker/lib/src/file_picker.dart b/packages/file_picker/lib/src/file_picker.dart index 86f40cca0..bab6ef6ac 100644 --- a/packages/file_picker/lib/src/file_picker.dart +++ b/packages/file_picker/lib/src/file_picker.dart @@ -129,7 +129,7 @@ abstract final class FilePicker { /// The [dialogTitle], if provided, will be used as the title for the directory picker dialog. /// The [initialDirectory], if provided, will be used as the initial directory path for the directory picker. /// - /// The [androidOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters + /// The [androidOptions], [darwinOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters /// allow for platform-specific configurations when configuring the directory picker. /// /// Returns a [String] containing the selected directory path, or `null` if canceled. @@ -137,6 +137,7 @@ abstract final class FilePicker { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -145,6 +146,7 @@ abstract final class FilePicker { dialogTitle: dialogTitle, initialDirectory: initialDirectory, androidOptions: androidOptions, + darwinOptions: darwinOptions, windowsOptions: windowsOptions, linuxOptions: linuxOptions, webOptions: webOptions, @@ -161,7 +163,7 @@ abstract final class FilePicker { /// The [initialDirectory], if provided, will be used as the initial directory path for the save file dialog. /// The [onFileSaving] callback, if provided, is triggered when the save dialog changes status. /// - /// The [windowsOptions], [linuxOptions], and [webOptions] parameters + /// The [darwinOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters /// allow for platform-specific configurations when configuring the save file dialog. /// /// Returns the [Uri] of the saved file, or `null` if the user canceled the operation. @@ -175,6 +177,7 @@ abstract final class FilePicker { FileType type = FileType.any, List? allowedExtensions, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -186,6 +189,7 @@ abstract final class FilePicker { dialogTitle: dialogTitle, initialDirectory: initialDirectory, onFileSaving: onFileSaving, + darwinOptions: darwinOptions, windowsOptions: windowsOptions, linuxOptions: linuxOptions, webOptions: webOptions, diff --git a/packages/file_picker/pubspec.yaml b/packages/file_picker/pubspec.yaml index 964c09b6c..871036504 100644 --- a/packages/file_picker/pubspec.yaml +++ b/packages/file_picker/pubspec.yaml @@ -9,25 +9,25 @@ topics: - storage - desktop - web -version: 13.1.0 +version: 14.0.0 resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 - android_file_picker: ^2.0.0 - file_picker_darwin: ^2.1.0 - file_picker_linux: ^2.0.0 - windows_file_picker: ^2.0.0 - file_picker_web: ^4.0.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + android_file_picker: ^3.0.0 + file_picker_darwin: ^3.0.0 + file_picker_linux: ^3.0.0 + windows_file_picker: ^3.0.0 + file_picker_web: ^5.0.0 + cross_file: ^0.4.0 dev_dependencies: flutter_test: diff --git a/packages/file_picker/test/file_picker_test.dart b/packages/file_picker/test/file_picker_test.dart index bf7e965ef..a48f1ad96 100644 --- a/packages/file_picker/test/file_picker_test.dart +++ b/packages/file_picker/test/file_picker_test.dart @@ -16,7 +16,7 @@ base class TestPlatformFile extends PlatformFile { final Uri uri; @override - XFile get xFile => XFile(path ?? ''); + XFile get xFile => XFile.fileSystem(path: path ?? ''); @override int? lengthSync() => 100; diff --git a/packages/file_picker_android/CHANGELOG.md b/packages/file_picker_android/CHANGELOG.md index 2f67a1973..8cba0dd36 100644 --- a/packages/file_picker_android/CHANGELOG.md +++ b/packages/file_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41, Dart 3.11 and Android SDK 24. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) + ## 2.0.1 - Fixed `pickFileAndDirectoryPaths()` returning percent-encoded paths that do not exist. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_android/android/build.gradle.kts b/packages/file_picker_android/android/build.gradle.kts index 558e3b3a0..434f0e858 100644 --- a/packages/file_picker_android/android/build.gradle.kts +++ b/packages/file_picker_android/android/build.gradle.kts @@ -64,7 +64,7 @@ configure { namespace = "com.mr.flutter.plugin.filepicker" defaultConfig { - minSdk = 21 + minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("proguard-rules.pro") } diff --git a/packages/file_picker_android/lib/src/android_platform_file.dart b/packages/file_picker_android/lib/src/android_platform_file.dart index 71078cc98..1af26b67d 100644 --- a/packages/file_picker_android/lib/src/android_platform_file.dart +++ b/packages/file_picker_android/lib/src/android_platform_file.dart @@ -51,7 +51,6 @@ base class AndroidPlatformFile extends PlatformFile { name: name, uri: uri, safHandle: safHandle, - xFile: path.isNotEmpty ? XFile(path, name: name) : null, bytesLength: data['size'] as int?, ); } @@ -68,15 +67,14 @@ base class AndroidPlatformFile extends PlatformFile { final XFile? _xFile; final int? _bytesLength; + /// A [FileSystemXFile] for a picked file cached on disk, or a + /// [ScopedStorageXFile] for a Storage Access Framework `content://` URI. @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// The size Android already reported for this file when it was picked. @override diff --git a/packages/file_picker_android/lib/src/file_picker_android.dart b/packages/file_picker_android/lib/src/file_picker_android.dart index e066bbe00..27f8e9ad7 100644 --- a/packages/file_picker_android/lib/src/file_picker_android.dart +++ b/packages/file_picker_android/lib/src/file_picker_android.dart @@ -159,6 +159,7 @@ class FilePickerAndroid extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const FilePickerAndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -195,6 +196,7 @@ class FilePickerAndroid extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), diff --git a/packages/file_picker_android/pubspec.yaml b/packages/file_picker_android/pubspec.yaml index 4b3646144..847bf38b9 100644 --- a/packages/file_picker_android/pubspec.yaml +++ b/packages/file_picker_android/pubspec.yaml @@ -1,6 +1,6 @@ name: android_file_picker description: Android implementation of the file_picker plugin, supporting file picking, saving, and Storage Access Framework (SAF) URI grants. -version: 2.0.1 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_android repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_android topics: @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -25,11 +25,12 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + cross_file: ^0.4.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_android/test/platform_file_read_test.dart b/packages/file_picker_android/test/platform_file_read_test.dart new file mode 100644 index 000000000..35f5dea0e --- /dev/null +++ b/packages/file_picker_android/test/platform_file_read_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:android_file_picker/android_file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = AndroidPlatformFile.fromMap({ + 'path': path, + 'name': 'notes.txt', + }); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_darwin/CHANGELOG.md b/packages/file_picker_darwin/CHANGELOG.md index 681675af2..f4d265843 100644 --- a/packages/file_picker_darwin/CHANGELOG.md +++ b/packages/file_picker_darwin/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41, Dart 3.11 and macOS 10.15. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- `DarwinOptions.acceptLabel` now also sets the confirm button text of `getDirectoryPath()` and `saveFile()` on macOS. [#1861](https://github.com/vicajilau/flutter_file_picker/issues/1861) + ## 2.2.0 - Added support for `DarwinOptions.orderedSelection`. Setting it to `false` restores swipe to select in the iOS photo library picker, which ordered selection disables. [#2241](https://github.com/vicajilau/flutter_file_picker/issues/2241) diff --git a/packages/file_picker_darwin/darwin/file_picker_darwin.podspec b/packages/file_picker_darwin/darwin/file_picker_darwin.podspec index e8f971642..5d4a36f73 100644 --- a/packages/file_picker_darwin/darwin/file_picker_darwin.podspec +++ b/packages/file_picker_darwin/darwin/file_picker_darwin.podspec @@ -21,7 +21,7 @@ iOS and macOS implementation of file_picker plugin s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '14.0' - s.osx.deployment_target = '10.13' + s.osx.deployment_target = '10.15' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift b/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift index dad3ad4dc..2b166941e 100644 --- a/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift +++ b/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "file_picker_darwin", platforms: [ .iOS("14.0"), - .macOS("10.13") + .macOS("10.15") ], products: [ .library(name: "file-picker-darwin", targets: ["file_picker_darwin"]) diff --git a/packages/file_picker_darwin/darwin/file_picker_darwin/Sources/file_picker_darwin/MacOSFilePickerHandler.swift b/packages/file_picker_darwin/darwin/file_picker_darwin/Sources/file_picker_darwin/MacOSFilePickerHandler.swift index ddd43e80f..d3639935c 100644 --- a/packages/file_picker_darwin/darwin/file_picker_darwin/Sources/file_picker_darwin/MacOSFilePickerHandler.swift +++ b/packages/file_picker_darwin/darwin/file_picker_darwin/Sources/file_picker_darwin/MacOSFilePickerHandler.swift @@ -228,6 +228,9 @@ final class MacOSFilePickerHandler: NSObject, FlutterStreamHandler { } dialog.message = title } + if let acceptLabel = args["acceptLabel"] as? String, !acceptLabel.isEmpty { + dialog.prompt = acceptLabel + } dialog.showsHiddenFiles = false dialog.allowsMultipleSelection = false dialog.canChooseDirectories = true @@ -269,6 +272,9 @@ final class MacOSFilePickerHandler: NSObject, FlutterStreamHandler { dialog.canCreateDirectories = true dialog.isExtensionHidden = true dialog.nameFieldStringValue = args["fileName"] as? String ?? "" + if let acceptLabel = args["acceptLabel"] as? String, !acceptLabel.isEmpty { + dialog.prompt = acceptLabel + } if let initialDirectory = args["initialDirectory"] as? String, !initialDirectory.isEmpty { diff --git a/packages/file_picker_darwin/lib/src/darwin_platform_file.dart b/packages/file_picker_darwin/lib/src/darwin_platform_file.dart index a56e54919..e6e8bc1b1 100644 --- a/packages/file_picker_darwin/lib/src/darwin_platform_file.dart +++ b/packages/file_picker_darwin/lib/src/darwin_platform_file.dart @@ -40,7 +40,6 @@ base class DarwinPlatformFile extends PlatformFile { return DarwinPlatformFile( name: name, uri: uri, - xFile: path.isNotEmpty ? XFile(path, name: name) : null, bytesLength: data['size'] as int?, ); } @@ -55,14 +54,11 @@ base class DarwinPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// The size iOS/macOS already reported for this file when it was picked. @override diff --git a/packages/file_picker_darwin/lib/src/file_picker_darwin.dart b/packages/file_picker_darwin/lib/src/file_picker_darwin.dart index b6c5e871a..82965fc69 100644 --- a/packages/file_picker_darwin/lib/src/file_picker_darwin.dart +++ b/packages/file_picker_darwin/lib/src/file_picker_darwin.dart @@ -169,6 +169,7 @@ class FilePickerDarwin extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -177,6 +178,7 @@ class FilePickerDarwin extends FilePickerPlatform { return await methodChannel.invokeMethod('dir', { 'dialogTitle': dialogTitle, 'initialDirectory': initialDirectory, + 'acceptLabel': darwinOptions.acceptLabel, }); } on PlatformException catch (ex) { print('[$_tag] Could not resolve directory path: ${ex.message}'); @@ -221,6 +223,7 @@ class FilePickerDarwin extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -248,6 +251,7 @@ class FilePickerDarwin extends FilePickerPlatform { "fileName": fileName, "fileType": mimeType, "initialDirectory": initialDirectory, + "acceptLabel": darwinOptions.acceptLabel, if (!isMacOS) "bytes": bytes, }); diff --git a/packages/file_picker_darwin/pubspec.yaml b/packages/file_picker_darwin/pubspec.yaml index 78367df4b..93133252e 100644 --- a/packages/file_picker_darwin/pubspec.yaml +++ b/packages/file_picker_darwin/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_darwin description: Darwin (iOS and macOS) implementation of the file_picker plugin, supporting native file picking, saving, and directory selection. -version: 2.2.0 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_darwin repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_darwin topics: @@ -12,8 +12,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -31,11 +31,12 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.1.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + cross_file: ^0.4.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_darwin/test/file_picker_darwin_test.dart b/packages/file_picker_darwin/test/file_picker_darwin_test.dart index c8bfd37f1..bd1e31e2f 100644 --- a/packages/file_picker_darwin/test/file_picker_darwin_test.dart +++ b/packages/file_picker_darwin/test/file_picker_darwin_test.dart @@ -115,6 +115,36 @@ void main() { expect(receivedLabel, isNull); }); + test('getDirectoryPath and saveFile send acceptLabel', () async { + final picker = FilePickerDarwin(); + final receivedLabels = {}; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(picker.methodChannel, (call) async { + receivedLabels[call.method] = + (call.arguments as Map)['acceptLabel'] as String?; + return null; + }); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(picker.methodChannel, null); + }); + + await picker.getDirectoryPath( + darwinOptions: const DarwinOptions(acceptLabel: 'Use folder'), + ); + await picker.saveFile( + fileName: 'a.txt', + bytes: Uint8List.fromList([1]), + mimeType: 'text/plain', + darwinOptions: const DarwinOptions(acceptLabel: 'Export'), + ); + expect(receivedLabels, {'dir': 'Use folder', 'save': 'Export'}); + + await picker.getDirectoryPath(); + expect(receivedLabels['dir'], isNull); + }); + test('pickFile and pickFiles send orderedSelection', () async { final picker = FilePickerDarwin(); bool? orderedSelection; diff --git a/packages/file_picker_darwin/test/platform_file_read_test.dart b/packages/file_picker_darwin/test/platform_file_read_test.dart new file mode 100644 index 000000000..53b847a04 --- /dev/null +++ b/packages/file_picker_darwin/test/platform_file_read_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:file_picker_darwin/file_picker_darwin.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = DarwinPlatformFile.fromMap({ + 'path': path, + 'name': 'notes.txt', + }); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_linux/CHANGELOG.md b/packages/file_picker_linux/CHANGELOG.md index afd366373..ba2009211 100644 --- a/packages/file_picker_linux/CHANGELOG.md +++ b/packages/file_picker_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the unused `bytes` parameter of `LinuxPlatformFile.fromPath()`. + ## 2.0.2 - Fixed `pickFileAndDirectoryPaths()` returning percent-encoded paths that do not exist. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_linux/lib/src/file_picker_linux.dart b/packages/file_picker_linux/lib/src/file_picker_linux.dart index 2a03b5d95..7a79b08b7 100644 --- a/packages/file_picker_linux/lib/src/file_picker_linux.dart +++ b/packages/file_picker_linux/lib/src/file_picker_linux.dart @@ -178,6 +178,7 @@ class FilePickerLinux extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const FilePickerLinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -234,6 +235,7 @@ class FilePickerLinux extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const FilePickerLinuxOptions(), WebOptions webOptions = const WebOptions(), diff --git a/packages/file_picker_linux/lib/src/linux_platform_file.dart b/packages/file_picker_linux/lib/src/linux_platform_file.dart index 1444efcbd..e894bf2b7 100644 --- a/packages/file_picker_linux/lib/src/linux_platform_file.dart +++ b/packages/file_picker_linux/lib/src/linux_platform_file.dart @@ -15,7 +15,7 @@ base class LinuxPlatformFile extends PlatformFile { }) : _xFile = xFile, _bytesLength = bytesLength; - factory LinuxPlatformFile.fromPath(String path, {Uint8List? bytes}) { + factory LinuxPlatformFile.fromPath(String path) { if (path.isEmpty) { throw ArgumentError( 'path cannot be empty when creating LinuxPlatformFile', @@ -23,12 +23,7 @@ base class LinuxPlatformFile extends PlatformFile { } final uri = Uri.file(path); final name = p.posix.basename(path); - return LinuxPlatformFile( - name: name, - uri: uri, - xFile: XFile(path, name: name, bytes: bytes), - bytesLength: bytes?.lengthInBytes, - ); + return LinuxPlatformFile(name: name, uri: uri); } @override @@ -41,14 +36,11 @@ base class LinuxPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// Only known when this file was created with its bytes already in hand. /// diff --git a/packages/file_picker_linux/pubspec.yaml b/packages/file_picker_linux/pubspec.yaml index 562f80f6d..9e77fddf3 100644 --- a/packages/file_picker_linux/pubspec.yaml +++ b/packages/file_picker_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_linux description: Linux implementation of the file_picker plugin using GTK3 and XDG Desktop Portals for native file and directory picking. -version: 2.0.2 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_linux repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_linux topics: @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -23,12 +23,13 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + cross_file: ^0.4.0 dbus: ^0.8.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_linux/test/file_picker_linux_test.dart b/packages/file_picker_linux/test/file_picker_linux_test.dart index f9a86545b..6ae1eb729 100644 --- a/packages/file_picker_linux/test/file_picker_linux_test.dart +++ b/packages/file_picker_linux/test/file_picker_linux_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:file_picker_linux/file_picker_linux.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -29,9 +27,10 @@ void main() { final withoutBytes = LinuxPlatformFile.fromPath('/tmp/test.png'); expect(withoutBytes.lengthSync(), isNull); - final withBytes = LinuxPlatformFile.fromPath( - '/tmp/test.png', - bytes: Uint8List.fromList([1, 2, 3]), + final withBytes = LinuxPlatformFile( + name: 'test.png', + uri: Uri.file('/tmp/test.png'), + bytesLength: 3, ); expect(withBytes.lengthSync(), equals(3)); }); diff --git a/packages/file_picker_linux/test/platform_file_read_test.dart b/packages/file_picker_linux/test/platform_file_read_test.dart new file mode 100644 index 000000000..42157dd24 --- /dev/null +++ b/packages/file_picker_linux/test/platform_file_read_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:file_picker_linux/file_picker_linux.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = LinuxPlatformFile.fromPath(path); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_platform_interface/CHANGELOG.md b/packages/file_picker_platform_interface/CHANGELOG.md index a6fd9a3d2..b4bde567f 100644 --- a/packages/file_picker_platform_interface/CHANGELOG.md +++ b/packages/file_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 5.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: `getDirectoryPath()` and `saveFile()` now take a `darwinOptions` parameter. Implementations must add it to their overrides. [#1861](https://github.com/vicajilau/flutter_file_picker/issues/1861) + ## 4.1.0 - Added `DarwinOptions.orderedSelection`, `true` by default. Set it to `false` to restore swipe to select in the iOS photo library picker. [#2241](https://github.com/vicajilau/flutter_file_picker/issues/2241) diff --git a/packages/file_picker_platform_interface/lib/src/file_picker_options/darwin_options.dart b/packages/file_picker_platform_interface/lib/src/file_picker_options/darwin_options.dart index d1afa0dca..ede704ed9 100644 --- a/packages/file_picker_platform_interface/lib/src/file_picker_options/darwin_options.dart +++ b/packages/file_picker_platform_interface/lib/src/file_picker_options/darwin_options.dart @@ -28,10 +28,10 @@ class DarwinOptions { /// The label for the confirm button of the file dialog. /// - /// Maps to `NSOpenPanel.prompt` on macOS. Only honored by `pickFile()` and - /// `pickFiles()`, the only entry points that accept [DarwinOptions] on - /// macOS. Has no effect on iOS, `UIDocumentPickerViewController` has no - /// equivalent there. + /// Maps to the `prompt` of `NSOpenPanel` on macOS for `pickFile()`, + /// `pickFiles()` and `getDirectoryPath()`, and of `NSSavePanel` for + /// `saveFile()`. Has no effect on iOS, `UIDocumentPickerViewController` has + /// no equivalent there. final String? acceptLabel; /// Whether the iOS photo library picker numbers selected media and returns diff --git a/packages/file_picker_platform_interface/lib/src/file_picker_platform_interface.dart b/packages/file_picker_platform_interface/lib/src/file_picker_platform_interface.dart index 54d526ad8..5c3d9e115 100644 --- a/packages/file_picker_platform_interface/lib/src/file_picker_platform_interface.dart +++ b/packages/file_picker_platform_interface/lib/src/file_picker_platform_interface.dart @@ -115,7 +115,7 @@ abstract class FilePickerPlatform extends PlatformInterface { /// The [dialogTitle], if provided, will be used as the title for the directory picker dialog. /// The [initialDirectory], if provided, will be used as the initial directory path for the directory picker. /// - /// The [androidOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters + /// The [androidOptions], [darwinOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters /// allow platform-specific configurations for the directory picker. /// /// Returns the absolute path of the selected directory, or `null` if the user canceled the operation. @@ -123,6 +123,7 @@ abstract class FilePickerPlatform extends PlatformInterface { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -151,7 +152,7 @@ abstract class FilePickerPlatform extends PlatformInterface { /// The [initialDirectory], if provided, will be used as the initial directory path for the save file dialog. /// The [onFileSaving] callback, if provided, is triggered when the save dialog changes status. /// - /// The [windowsOptions], [linuxOptions], and [webOptions] parameters + /// The [darwinOptions], [windowsOptions], [linuxOptions], and [webOptions] parameters /// allow platform-specific configurations for the save file dialog. /// /// Returns the [Uri] of the saved file, or `null` if the user canceled the operation. @@ -163,6 +164,7 @@ abstract class FilePickerPlatform extends PlatformInterface { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), diff --git a/packages/file_picker_platform_interface/pubspec.yaml b/packages/file_picker_platform_interface/pubspec.yaml index b31ac2de3..590a21413 100644 --- a/packages/file_picker_platform_interface/pubspec.yaml +++ b/packages/file_picker_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_platform_interface description: A common platform interface for the file_picker plugin, defining the shared API surface and data contracts across platforms. -version: 4.1.0 +version: 5.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_platform_interface repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_platform_interface topics: @@ -10,14 +10,14 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.1.8 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 path: ^1.9.1 dev_dependencies: diff --git a/packages/file_picker_platform_interface/test/platform_file_test.dart b/packages/file_picker_platform_interface/test/platform_file_test.dart index af7481e92..5dc325476 100644 --- a/packages/file_picker_platform_interface/test/platform_file_test.dart +++ b/packages/file_picker_platform_interface/test/platform_file_test.dart @@ -14,7 +14,7 @@ base class _TestPlatformFile extends PlatformFile { Uri get uri => Uri.file(name); @override - XFile get xFile => XFile(name); + XFile get xFile => XFile.fileSystem(path: name); @override int? lengthSync() => null; diff --git a/packages/file_picker_web/CHANGELOG.md b/packages/file_picker_web/CHANGELOG.md index 8596955d5..f1b233394 100644 --- a/packages/file_picker_web/CHANGELOG.md +++ b/packages/file_picker_web/CHANGELOG.md @@ -1,3 +1,10 @@ +## 5.0.0 + +- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. Picked files are no longer read at pick time, use `readAsBytes()`/`readAsByteStream()`. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the `bytes` and `readStream` parameters of `WebPlatformFile`. +- `readAsByteStream()` emits chunks of `WebPlatformFile.streamChunkSize` (1 MiB), except the last one, which may be shorter. +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. + ## 4.1.0 - `readAsByteStream()` now always emits evenly sized chunks of `WebPlatformFile.streamChunkSize` (1 MiB), except the last one, with or without `withReadStream`. With `withReadStream` the chunks used to be 1,000,000 bytes. [#2223](https://github.com/vicajilau/flutter_file_picker/issues/2223) diff --git a/packages/file_picker_web/lib/src/file_picker_web.dart b/packages/file_picker_web/lib/src/file_picker_web.dart index d994cde7b..e3354f340 100644 --- a/packages/file_picker_web/lib/src/file_picker_web.dart +++ b/packages/file_picker_web/lib/src/file_picker_web.dart @@ -2,15 +2,14 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:path/path.dart' as p; import 'package:web/web.dart'; import 'file_picker_web_options.dart'; -import 'indexed_task_runner.dart'; -import 'platform_file_web_fetch.dart'; -import 'preload_policy.dart'; import 'web_file_input_session.dart'; import 'web_platform_file.dart'; @@ -55,6 +54,7 @@ class FilePickerWeb extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -141,85 +141,36 @@ class FilePickerWeb extends FilePickerPlatform { return files ?? []; } - /// Processes the selected [FileList] according to [webOptions] and returns - /// a list of [PlatformFile] instances. + /// Wraps each selected [File] in a [WebPlatformFile] without reading it. /// - /// Reads files one at a time when [FilePickerWebOptions.readSequential] is - /// `true`, or concurrently otherwise; either way the result preserves the - /// original selection order. + /// The content is read on demand through [PlatformFile.readAsBytes] and + /// [PlatformFile.readAsByteStream]. Future> _processSelectedFiles( FileList files, FilePickerWebOptions webOptions, ) async { - Future processFileAt(int index) async { - final file = files.item(index); - if (file == null) return null; - - if (webOptions.withReadStream) { - return _createWebPlatformFile( - file: file, - readStream: streamBlobInChunks(file), - ); - } - - if (!shouldPreloadBytes(file.size, withData: webOptions.withData)) { - return _createWebPlatformFile(file: file); - } - - final bytes = await _readSingleFileBytes(file); - return _createWebPlatformFile(file: file, bytes: bytes); - } - - final results = await runIndexedTasks( - files.length, - processFileAt, - sequential: webOptions.readSequential, - ); - return results.whereType().toList(); - } - - /// Reads an HTML [File] content into a [Uint8List] using [FileReader]. - Future _readSingleFileBytes(File file) async { - final completer = Completer(); - final reader = FileReader(); - - reader.addEventListener( - 'loadend', - ((Event _) { - if (!completer.isCompleted) { - final byteBuffer = (reader.result as JSArrayBuffer?)?.toDart; - completer.complete(byteBuffer?.asUint8List()); - } - }).toJS, - ); - - reader.addEventListener( - 'error', - ((Event _) { - if (!completer.isCompleted) { - completer.complete(null); - } - }).toJS, - ); - - reader.readAsArrayBuffer(file); - return completer.future; + return [ + for (var i = 0; i < files.length; i++) + if (files.item(i) case final file?) _createWebPlatformFile(file), + ]; } - /// Creates a [WebPlatformFile] from an HTML [File], resolving its `blob:` URI. + /// Creates a [WebPlatformFile] backed by the picked [file] itself. /// - /// The URI always points at the picked [file] itself, which avoids creating a copy. - WebPlatformFile _createWebPlatformFile({ - required File file, - Uint8List? bytes, - Stream? readStream, - }) { + /// Its `blob:` URI is not revoked automatically, so it stays valid for as + /// long as the page is open. + WebPlatformFile _createWebPlatformFile(File file) { + final xFile = ScopedStorageXFile.fromCreationParams( + WebScopedStorageXFileCreationParams.fromBlob( + file, + autoRevokeObjectUrl: false, + ), + ); return WebPlatformFile( name: file.name, - uri: Uri.parse(URL.createObjectURL(file)), - bytesLength: bytes != null ? bytes.length : file.size, - bytes: bytes, - readStream: readStream, + uri: Uri.parse(xFile.uri), + xFile: xFile, + bytesLength: file.size, ); } @@ -235,6 +186,7 @@ class FilePickerWeb extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const WindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), diff --git a/packages/file_picker_web/lib/src/file_picker_web_options.dart b/packages/file_picker_web/lib/src/file_picker_web_options.dart index 627d42ee4..6d7f91532 100644 --- a/packages/file_picker_web/lib/src/file_picker_web_options.dart +++ b/packages/file_picker_web/lib/src/file_picker_web_options.dart @@ -2,54 +2,9 @@ import 'package:file_picker_platform_interface/file_picker_platform_interface.da /// Configuration options specific to the Web platform. final class FilePickerWebOptions extends WebOptions { - /// Whether to read each picked file into memory at pick time. - /// - /// Files larger than 2 GB are never preloaded, since browsers cannot hold - /// them in a single buffer. Their content can be read on demand through - /// `PlatformFile.readAsBytes()` and `readAsByteStream()` instead. - @Deprecated( - 'Picked files are read on demand by PlatformFile.readAsBytes() and ' - 'readAsByteStream(). This option will be removed in file_picker v14.0.0.', - ) - final bool withData; - - /// Whether to create a read stream for each picked file. - @Deprecated( - 'PlatformFile.readAsByteStream() already reads on demand in evenly sized ' - 'chunks. This option will be removed in file_picker v14.0.0.', - ) - final bool withReadStream; - - /// Whether to read multiple files one at a time instead of concurrently. - /// - /// The result always preserves the original selection order, regardless - /// of this setting. - @Deprecated( - 'Only affects the deprecated withData preload. This option will be ' - 'removed in file_picker v14.0.0.', - ) - final bool readSequential; - /// Whether to cancel upload when window loses focus. final bool cancelUploadOnWindowBlur; /// Creates an instance of [FilePickerWebOptions]. - const FilePickerWebOptions({ - @Deprecated( - 'Picked files are read on demand by PlatformFile.readAsBytes() and ' - 'readAsByteStream(). This option will be removed in file_picker v14.0.0.', - ) - this.withData = true, - @Deprecated( - 'PlatformFile.readAsByteStream() already reads on demand in evenly ' - 'sized chunks. This option will be removed in file_picker v14.0.0.', - ) - this.withReadStream = false, - @Deprecated( - 'Only affects the deprecated withData preload. This option will be ' - 'removed in file_picker v14.0.0.', - ) - this.readSequential = false, - this.cancelUploadOnWindowBlur = true, - }); + const FilePickerWebOptions({this.cancelUploadOnWindowBlur = true}); } diff --git a/packages/file_picker_web/lib/src/indexed_task_runner.dart b/packages/file_picker_web/lib/src/indexed_task_runner.dart deleted file mode 100644 index c8f4243b3..000000000 --- a/packages/file_picker_web/lib/src/indexed_task_runner.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Runs [task] once for each index in `[0, length)`, either one at a time -/// ([sequential] `true`) or concurrently ([sequential] `false`). -/// -/// Returns the results in index order regardless of completion order, so -/// callers can pick concurrency without also giving up on ordering. -Future> runIndexedTasks( - int length, - Future Function(int index) task, { - required bool sequential, -}) async { - if (length == 0) return []; - - if (sequential) { - final results = []; - for (var i = 0; i < length; i++) { - results.add(await task(i)); - } - return results; - } - - // Future.wait() already returns results in the order the futures were - // passed in, not completion order, so no manual index bookkeeping is - // needed here. - return Future.wait([for (var i = 0; i < length; i++) task(i)]); -} diff --git a/packages/file_picker_web/lib/src/platform_file_web_fetch.dart b/packages/file_picker_web/lib/src/platform_file_web_fetch.dart deleted file mode 100644 index 0000189a2..000000000 --- a/packages/file_picker_web/lib/src/platform_file_web_fetch.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:js_interop'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:web/web.dart'; - -@JS('fetch') -external JSPromise _fetchJs(JSString url); - -/// Interop extension type representing a Web `Response` JS object. -extension type _Response(JSObject _) implements JSObject { - external JSPromise arrayBuffer(); - external JSPromise blob(); -} - -/// Fetches the bytes of a web-only path (`blob:` or `data:` URL). -/// -/// Returns the full file bytes (`Uint8List`) using `fetch(...).arrayBuffer()` -/// for `blob:` URLs, or parses `data:` URIs. Returns `null` if [path] is not a -/// web URL, so the caller can fall back to another source. Throws if the -/// content of a web URL cannot be read. -Future fetchBytesFromWebPath(String path) async { - if (!_isWebPath(path)) return null; - - if (path.startsWith('data:')) { - final uriData = Uri.parse(path).data; - - if (uriData == null) { - return null; - } - - return uriData.contentAsBytes(); - } - - final response = _Response(await _fetchJs(path.toJS).toDart); - final buffer = await response.arrayBuffer().toDart; - return buffer.toDart.asUint8List(); -} - -bool _isWebPath(String path) => - path.startsWith('blob:') || path.startsWith('data:'); - -/// Attempts to create a streaming `Stream` from a web-only path -/// (`blob:` or `data:` URL). -/// -/// Returns `null` if [path] is not a web URL, so the caller can fall back to -/// another source. Read failures are emitted as errors on the stream. -Stream? fetchStreamFromWebPath(String path) { - if (!_isWebPath(path)) return null; - - return _streamFromWebPath(path); -} - -/// The size of the chunks emitted by [streamBlobInChunks]. -const int webStreamChunkSize = 1024 * 1024; - -/// Reads a `blob:` or `data:` URL and emits its bytes in evenly sized chunks. -Stream _streamFromWebPath(String path) async* { - final response = _Response(await _fetchJs(path.toJS).toDart); - yield* streamBlobInChunks(await response.blob().toDart); -} - -/// Emits the content of [blob] in chunks of [webStreamChunkSize] bytes, -/// except the last one, which may be shorter. -Stream streamBlobInChunks(Blob blob) async* { - for (var start = 0; start < blob.size; start += webStreamChunkSize) { - final end = min(start + webStreamChunkSize, blob.size); - final buffer = await blob.slice(start, end).arrayBuffer().toDart; - yield buffer.toDart.asUint8List(); - } -} diff --git a/packages/file_picker_web/lib/src/preload_policy.dart b/packages/file_picker_web/lib/src/preload_policy.dart deleted file mode 100644 index d9780de4d..000000000 --- a/packages/file_picker_web/lib/src/preload_policy.dart +++ /dev/null @@ -1,13 +0,0 @@ -/// The largest file, in bytes, whose content is preloaded into memory when -/// `FilePickerWebOptions.withData` is `true`. -/// -/// Browsers cannot reliably hold a single buffer above 2 GB: Chrome fails the -/// read (and can crash the tab when a debugger is attached) and Firefox -/// rejects buffers that large. Larger files are still read on demand through -/// `PlatformFile.readAsBytes()` and `readAsByteStream()`. -const int maxPreloadBytes = 2 * 1024 * 1024 * 1024; - -/// Whether a picked file of [size] bytes should be read into memory at pick -/// time. -bool shouldPreloadBytes(int size, {required bool withData}) => - withData && size <= maxPreloadBytes; diff --git a/packages/file_picker_web/lib/src/web_platform_file.dart b/packages/file_picker_web/lib/src/web_platform_file.dart index 19c727725..694108dbf 100644 --- a/packages/file_picker_web/lib/src/web_platform_file.dart +++ b/packages/file_picker_web/lib/src/web_platform_file.dart @@ -1,32 +1,29 @@ import 'dart:async'; +import 'dart:js_interop'; +import 'dart:math'; import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; -import 'platform_file_web_fetch.dart'; - /// A Web-specific implementation of [PlatformFile]. /// -/// Wraps files selected or processed in a web environment, providing -/// access to file metadata, underlying bytes, stream readers, and `blob:` or -/// `data:` URIs. +/// Wraps a file selected in a web environment. Its content is read on demand +/// through [xFile], from a `blob:` or `data:` URI. base class WebPlatformFile extends PlatformFile { /// Creates a new [WebPlatformFile] instance. /// - /// Requires a file [name] and [uri]. Optional parameters include preloaded - /// [bytes], a custom [readStream], file size in [bytesLength], or an underlying [xFile]. + /// Requires a file [name] and [uri]. Optional parameters include the file + /// size in [bytesLength] and an underlying [xFile]. Without [xFile], the + /// content is read from [uri]. WebPlatformFile({ required this.name, required this.uri, XFile? xFile, int? bytesLength, - Uint8List? bytes, - Stream? readStream, }) : _xFile = xFile, - _bytesLength = bytesLength, - _bytes = bytes, - _readStream = readStream { + _bytesLength = bytesLength { if (name.isEmpty) { throw ArgumentError('name cannot be empty'); } @@ -45,23 +42,16 @@ base class WebPlatformFile extends PlatformFile { final XFile? _xFile; final int? _bytesLength; - final Uint8List? _bytes; - final Stream? _readStream; /// Returns an [XFile] instance representing this web file. @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - return XFile(uri.toString(), name: name, bytes: _bytes); - } + XFile get xFile => _xFile ?? XFile.scopedStorage(uri: uri.toString()); - /// The browser's `File.size`, or the loaded bytes' length. + /// The browser's `File.size`, if known. @override int? lengthSync() { final len = _bytesLength; - if (len != null && len > 0) return len; - return _bytes?.length; + return (len != null && len > 0) ? len : null; } /// Asynchronously calculates and returns the size of the file in bytes. @@ -69,8 +59,6 @@ base class WebPlatformFile extends PlatformFile { Future length() async { final len = _bytesLength; if (len != null && len > 0) return len; - final bytes = _bytes; - if (bytes != null) return bytes.length; try { return await xFile.length(); } catch (_) { @@ -78,44 +66,34 @@ base class WebPlatformFile extends PlatformFile { } } - /// Asynchronously reads the file content as a byte array (`Uint8List`). + /// Reads the file content as a byte array (`Uint8List`). /// - /// If bytes are cached or readable from a `blob:`/`data:` URL, they will be - /// retrieved directly; otherwise falls back to [xFile]. + /// Throws if the content cannot be read. @override - Future readAsBytes() async { - final bytes = _bytes; - if (bytes != null) return bytes; - - // Try fetching from blob or data URL - final fetched = await fetchBytesFromWebPath(uri.toString()); - if (fetched != null) return fetched; - - return xFile.readAsBytes(); - } + Future readAsBytes() => xFile.readAsBytes(); /// The size of the chunks emitted by [readAsByteStream]. - static const int streamChunkSize = webStreamChunkSize; + static const int streamChunkSize = 1024 * 1024; - /// Asynchronously opens a stream to read the file content in chunks. + /// Opens a stream to read the file content in chunks. /// /// Every chunk is [streamChunkSize] bytes long except the last one, which - /// may be shorter, so consumers can rely on evenly sized buffers. + /// may be shorter, so consumers can rely on evenly sized buffers. Read + /// failures are emitted as errors on the stream. @override Stream readAsByteStream() async* { - final readStream = _readStream; - if (readStream != null) { - yield* readStream; + final extension = xFile.getExtension(); + if (extension == null) { + yield* xFile.openRead(); return; } - final webStream = fetchStreamFromWebPath(uri.toString()); - if (webStream != null) { - yield* webStream; - return; + final blob = await extension.getBlob(); + for (var start = 0; start < blob.size; start += streamChunkSize) { + final end = min(start + streamChunkSize, blob.size); + final buffer = await blob.slice(start, end).arrayBuffer().toDart; + yield buffer.toDart.asUint8List(); } - - yield* xFile.openRead(); } @override diff --git a/packages/file_picker_web/pubspec.yaml b/packages/file_picker_web/pubspec.yaml index 9feab4080..d9655e44c 100644 --- a/packages/file_picker_web/pubspec.yaml +++ b/packages/file_picker_web/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_web description: Web platform implementation of the file_picker plugin, providing browser file selection, file streaming, and file loading. -version: 4.1.0 +version: 5.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web topics: @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -26,8 +26,9 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + cross_file: ^0.4.0 + cross_file_web: ^1.0.0 meta: ^1.17.0 path: ^1.9.1 web: ^1.1.1 diff --git a/packages/file_picker_web/test/file_picker_web_pick_test.dart b/packages/file_picker_web/test/file_picker_web_pick_test.dart index 86fdea20f..0dd4b7ecb 100644 --- a/packages/file_picker_web/test/file_picker_web_pick_test.dart +++ b/packages/file_picker_web/test/file_picker_web_pick_test.dart @@ -5,21 +5,20 @@ import 'dart:convert'; import 'dart:js_interop'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_web/file_picker_web.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; void main() { - setUpAll(() => FilePickerWeb.registerWith(webPluginRegistrar)); + setUpAll(() { + FilePickerWeb.registerWith(webPluginRegistrar); + CrossFileWeb.registerWith(webPluginRegistrar); + }); - Future> pickFiles( - List files, { - WebOptions webOptions = const WebOptions(), - }) { - final result = FilePickerPlatform.instance.pickFiles( - webOptions: webOptions, - ); + Future> pickFiles(List files) { + final result = FilePickerPlatform.instance.pickFiles(); final input = document.querySelector('flt-file-picker-inputs input')! @@ -51,29 +50,16 @@ void main() { expect(await files.single.readAsBytes(), isEmpty); }); - Future expectEvenChunks(List files) async { + test('picked files stream evenly sized chunks', () async { + final content = 'a' * (WebPlatformFile.streamChunkSize + 10); + final files = await pickFiles([ + fileWith(name: 'big.txt', content: content), + ]); + final chunks = await files.single.readAsByteStream().toList(); expect(chunks.map((chunk) => chunk.length), [ WebPlatformFile.streamChunkSize, 10, ]); - } - - String bigContent() => 'a' * (WebPlatformFile.streamChunkSize + 10); - - test('picked files stream evenly sized chunks', () async { - await expectEvenChunks( - await pickFiles([fileWith(name: 'big.txt', content: bigContent())]), - ); - }); - - test('picked files stream evenly sized chunks with withReadStream', () async { - await expectEvenChunks( - await pickFiles( - [fileWith(name: 'big.txt', content: bigContent())], - // ignore: deprecated_member_use_from_same_package - webOptions: const FilePickerWebOptions(withReadStream: true), - ), - ); }); } diff --git a/packages/file_picker_web/test/indexed_task_runner_test.dart b/packages/file_picker_web/test/indexed_task_runner_test.dart deleted file mode 100644 index 24f0569e5..000000000 --- a/packages/file_picker_web/test/indexed_task_runner_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:file_picker_web/src/indexed_task_runner.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('runIndexedTasks', () { - test('sequential runs one at a time and preserves order', () async { - var concurrent = 0; - var maxConcurrent = 0; - final startOrder = []; - - final results = await runIndexedTasks(5, (index) async { - concurrent++; - maxConcurrent = concurrent > maxConcurrent ? concurrent : maxConcurrent; - startOrder.add(index); - // Earlier indices are slower, so if the next task started before - // this one finished, sequential mode would still be broken. - await Future.delayed(Duration(milliseconds: 5 * (5 - index))); - concurrent--; - return index; - }, sequential: true); - - expect(maxConcurrent, 1); - expect(startOrder, [0, 1, 2, 3, 4]); - expect(results, [0, 1, 2, 3, 4]); - }); - - test( - 'concurrent runs more than one task at a time and still preserves order', - () async { - var concurrent = 0; - var maxConcurrent = 0; - final completionOrder = []; - - final results = await runIndexedTasks(5, (index) async { - concurrent++; - maxConcurrent = concurrent > maxConcurrent - ? concurrent - : maxConcurrent; - // Reverse delays so completion order is the opposite of index - // order, proving the result list isn't just built in completion - // order. - await Future.delayed(Duration(milliseconds: 5 * (5 - index))); - concurrent--; - completionOrder.add(index); - return index; - }, sequential: false); - - expect(maxConcurrent, greaterThan(1)); - expect(completionOrder, [4, 3, 2, 1, 0]); - expect(results, [0, 1, 2, 3, 4]); - }, - ); - - test('returns an empty list for zero length', () async { - final results = await runIndexedTasks( - 0, - (index) async => index, - sequential: false, - ); - expect(results, isEmpty); - }); - }); -} diff --git a/packages/file_picker_web/test/preload_policy_test.dart b/packages/file_picker_web/test/preload_policy_test.dart deleted file mode 100644 index 69334dfde..000000000 --- a/packages/file_picker_web/test/preload_policy_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:file_picker_web/src/preload_policy.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('preloads files up to 2 GB when withData is true', () { - expect(shouldPreloadBytes(0, withData: true), isTrue); - expect(shouldPreloadBytes(maxPreloadBytes, withData: true), isTrue); - }); - - test('does not preload files larger than 2 GB', () { - expect(shouldPreloadBytes(maxPreloadBytes + 1, withData: true), isFalse); - expect(shouldPreloadBytes(2400 * 1024 * 1024, withData: true), isFalse); - }); - - test('never preloads when withData is false', () { - expect(shouldPreloadBytes(1, withData: false), isFalse); - }); -} diff --git a/packages/file_picker_web/test/web_options_test.dart b/packages/file_picker_web/test/web_options_test.dart index e4b8578d9..b881d6680 100644 --- a/packages/file_picker_web/test/web_options_test.dart +++ b/packages/file_picker_web/test/web_options_test.dart @@ -2,11 +2,13 @@ import 'package:file_picker_web/src/file_picker_web_options.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('FilePickerWebOptions initializes with default properties', () { - const options = FilePickerWebOptions(withData: false, withReadStream: true); - expect(options.withData, isFalse); - expect(options.withReadStream, isTrue); - expect(options.readSequential, isFalse); - expect(options.cancelUploadOnWindowBlur, isTrue); + test('FilePickerWebOptions cancels on window blur by default', () { + expect(const FilePickerWebOptions().cancelUploadOnWindowBlur, isTrue); + expect( + const FilePickerWebOptions( + cancelUploadOnWindowBlur: false, + ).cancelUploadOnWindowBlur, + isFalse, + ); }); } diff --git a/packages/file_picker_web/test/web_platform_file_read_test.dart b/packages/file_picker_web/test/web_platform_file_read_test.dart index 2114f64c9..6cb306593 100644 --- a/packages/file_picker_web/test/web_platform_file_read_test.dart +++ b/packages/file_picker_web/test/web_platform_file_read_test.dart @@ -5,11 +5,15 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_web/file_picker_web.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; void main() { + setUpAll(() => CrossFileWeb.registerWith(webPluginRegistrar)); + final content = Uint8List.fromList(utf8.encode('hello file_picker')); String createBlobUrl() => URL.createObjectURL(Blob([content.toJS].toJS)); diff --git a/packages/file_picker_windows/CHANGELOG.md b/packages/file_picker_windows/CHANGELOG.md index 16a771912..d5f116756 100644 --- a/packages/file_picker_windows/CHANGELOG.md +++ b/packages/file_picker_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the unused `bytes` parameter of `WindowsPlatformFile.fromPath()`. + ## 2.0.1 - Fixed `pickFileAndDirectoryPaths()` returning URI paths instead of Windows paths. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_windows/lib/src/file_picker_windows.dart b/packages/file_picker_windows/lib/src/file_picker_windows.dart index 6f00f1a02..4bf3ba268 100644 --- a/packages/file_picker_windows/lib/src/file_picker_windows.dart +++ b/packages/file_picker_windows/lib/src/file_picker_windows.dart @@ -129,6 +129,7 @@ class FilePickerWindows extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, AndroidOptions androidOptions = const AndroidOptions(), + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const FilePickerWindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), @@ -227,6 +228,7 @@ class FilePickerWindows extends FilePickerPlatform { String? dialogTitle, String? initialDirectory, Function(FilePickerStatus)? onFileSaving, + DarwinOptions darwinOptions = const DarwinOptions(), WindowsOptions windowsOptions = const FilePickerWindowsOptions(), LinuxOptions linuxOptions = const LinuxOptions(), WebOptions webOptions = const WebOptions(), diff --git a/packages/file_picker_windows/lib/src/windows_platform_file.dart b/packages/file_picker_windows/lib/src/windows_platform_file.dart index 1edcbd042..11d10cd83 100644 --- a/packages/file_picker_windows/lib/src/windows_platform_file.dart +++ b/packages/file_picker_windows/lib/src/windows_platform_file.dart @@ -15,7 +15,7 @@ base class WindowsPlatformFile extends PlatformFile { }) : _xFile = xFile, _bytesLength = bytesLength; - factory WindowsPlatformFile.fromPath(String path, {Uint8List? bytes}) { + factory WindowsPlatformFile.fromPath(String path) { if (path.isEmpty) { throw ArgumentError( 'path cannot be empty when creating WindowsPlatformFile', @@ -23,12 +23,7 @@ base class WindowsPlatformFile extends PlatformFile { } final uri = Uri.file(path, windows: true); final name = p.windows.basename(path); - return WindowsPlatformFile( - name: name, - uri: uri, - xFile: XFile(path, name: name, bytes: bytes), - bytesLength: bytes?.lengthInBytes, - ); + return WindowsPlatformFile(name: name, uri: uri); } @override @@ -41,14 +36,11 @@ base class WindowsPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// Only known when this file was created with its bytes already in hand. /// diff --git a/packages/file_picker_windows/pubspec.yaml b/packages/file_picker_windows/pubspec.yaml index 74f1bc017..a6268b10c 100644 --- a/packages/file_picker_windows/pubspec.yaml +++ b/packages/file_picker_windows/pubspec.yaml @@ -1,6 +1,6 @@ name: windows_file_picker description: Windows implementation of the file_picker plugin using Win32 COM APIs for native file and directory picking. -version: 2.0.1 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_windows repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_windows topics: @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -23,14 +23,15 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + file_picker_platform_interface: ^5.0.0 + cross_file: ^0.4.0 ffi: ^2.2.0 meta: ^1.17.0 path: ^1.9.1 win32: ^6.3.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_windows/test/file_picker_windows_test.dart b/packages/file_picker_windows/test/file_picker_windows_test.dart index f6eaadc23..26481fa01 100644 --- a/packages/file_picker_windows/test/file_picker_windows_test.dart +++ b/packages/file_picker_windows/test/file_picker_windows_test.dart @@ -1,5 +1,4 @@ import 'dart:isolate'; -import 'dart:typed_data'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:windows_file_picker/src/open_save_file_args.dart'; @@ -35,9 +34,10 @@ void main() { ); expect(withoutBytes.lengthSync(), isNull); - final withBytes = WindowsPlatformFile.fromPath( - r'C:\Users\Test\file.txt', - bytes: Uint8List.fromList([1, 2, 3]), + final withBytes = WindowsPlatformFile( + name: 'file.txt', + uri: Uri.file(r'C:\Users\Test\file.txt', windows: true), + bytesLength: 3, ); expect(withBytes.lengthSync(), equals(3)); }, diff --git a/packages/file_picker_windows/test/platform_file_read_test.dart b/packages/file_picker_windows/test/platform_file_read_test.dart new file mode 100644 index 000000000..93a9cb1f0 --- /dev/null +++ b/packages/file_picker_windows/test/platform_file_read_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:windows_file_picker/windows_file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = WindowsPlatformFile.fromPath(path); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5229ebdca..1257e67fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,8 +42,8 @@ melos: description: Run the web integration_test suite against the example app. environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dev_dependencies: flutter_lints: ^6.0.0