From 25e1d87113359a767dd1985db8adacccbe4e515f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 23 Jun 2023 12:43:45 -0400 Subject: [PATCH] [file_selector] Add file group to save return value (#4222) This deprecates `getSavePath`, which returned a target path string, in favor of a new `getSaveLocation`, which returns an object containing both a path and, optionally, a selected file group. This allows clients to use the selected group when deciding what path to use when saving (see discussion in linked issue). This includes an implementation for Windows. It will also apply to Linux, and I've verified that the structure works, but it's not included here because it requires some non-trivial refactoring in the Linux implementation (we can't get the current index, only the current filter object pointer, which means we need to pass more data around between the various functions to map back to an index... and it's GObject so making internal data utility classes is fiddly.) For now Linux just always returns a null group, and we can add it later. Most of https://github.com/flutter/flutter/issues/107093 --- .../file_selector/file_selector/CHANGELOG.md | 3 +- .../file_selector/file_selector/README.md | 7 +- .../lib/readme_standalone_excerpts.dart | 7 +- .../example/lib/save_text_page.dart | 6 +- .../file_selector/lib/file_selector.dart | 50 ++++++-- .../file_selector/file_selector/pubspec.yaml | 12 +- .../test/file_selector_test.dart | 111 ++++++++++++++++-- 7 files changed, 165 insertions(+), 31 deletions(-) diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 98d02784d7..5215787e07 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.4 +* Adds `getSaveLocation` and deprecates `getSavePath`. * Updates minimum supported macOS version to 10.14. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index 860be7fbe9..dac9746891 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -68,8 +68,9 @@ final List files = await openFiles(acceptedTypeGroups: [ ```dart const String fileName = 'suggested_name.txt'; -final String? path = await getSavePath(suggestedName: fileName); -if (path == null) { +final FileSaveLocation? result = + await getSaveLocation(suggestedName: fileName); +if (result == null) { // Operation was canceled by the user. return; } @@ -78,7 +79,7 @@ final Uint8List fileData = Uint8List.fromList('Hello World!'.codeUnits); const String mimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: mimeType, name: fileName); -await textFile.saveTo(path); +await textFile.saveTo(result.path); ``` #### Get a directory path diff --git a/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart b/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart index e12be75171..cc0a051e21 100644 --- a/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart +++ b/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart @@ -39,8 +39,9 @@ class _MyAppState extends State { Future saveFile() async { // #docregion Save const String fileName = 'suggested_name.txt'; - final String? path = await getSavePath(suggestedName: fileName); - if (path == null) { + final FileSaveLocation? result = + await getSaveLocation(suggestedName: fileName); + if (result == null) { // Operation was canceled by the user. return; } @@ -49,7 +50,7 @@ class _MyAppState extends State { const String mimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: mimeType, name: fileName); - await textFile.saveTo(path); + await textFile.saveTo(result.path); // #enddocregion Save } diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart index 751b91c793..e782530914 100644 --- a/packages/file_selector/file_selector/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -24,11 +24,11 @@ class SaveTextPage extends StatelessWidget { // file will be saved. In most cases, this parameter should not be provided. final String initialDirectory = (await getApplicationDocumentsDirectory()).path; - final String? path = await getSavePath( + final FileSaveLocation? result = await getSaveLocation( initialDirectory: initialDirectory, suggestedName: fileName, ); - if (path == null) { + if (result == null) { // Operation was canceled by the user. return; } @@ -39,7 +39,7 @@ class SaveTextPage extends StatelessWidget { final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); - await textFile.saveTo(path); + await textFile.saveTo(result.path); } @override diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart index c75cb58233..e1739eda0c 100644 --- a/packages/file_selector/file_selector/lib/file_selector.dart +++ b/packages/file_selector/file_selector/lib/file_selector.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; export 'package:file_selector_platform_interface/file_selector_platform_interface.dart' - show XFile, XTypeGroup; + show FileSaveLocation, XFile, XTypeGroup; /// Opens a file selection dialog and returns the path chosen by the user. /// @@ -92,20 +92,54 @@ Future> openFiles({ /// When not provided, the default OS label is used (for example, "Save"). /// /// Returns `null` if the user cancels the operation. +@Deprecated('Use getSaveLocation instead') Future getSavePath({ List acceptedTypeGroups = const [], String? initialDirectory, String? suggestedName, String? confirmButtonText, }) async { - // TODO(stuartmorgan): Update this to getSaveLocation in the next federated - // change PR. - // ignore: deprecated_member_use - return FileSelectorPlatform.instance.getSavePath( + return (await getSaveLocation( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText)) + ?.path; +} + +/// Opens a save dialog and returns the target path chosen by the user. +/// +/// [acceptedTypeGroups] is a list of file type groups that can be selected in +/// the dialog. How this is displayed depends on the pltaform, for example: +/// - On Windows and Linux, each group will be an entry in a list of filter +/// options. +/// - On macOS, the union of all types allowed by all of the groups will be +/// allowed. +/// Throws an [ArgumentError] if any type groups do not include filters +/// supported by the current platform. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. +/// +/// [suggestedName] is initial value of file name. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Save"). +/// +/// Returns `null` if the user cancels the operation. +Future getSaveLocation({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getSaveLocation( acceptedTypeGroups: acceptedTypeGroups, - initialDirectory: initialDirectory, - suggestedName: suggestedName, - confirmButtonText: confirmButtonText); + options: SaveDialogOptions( + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText)); } /// Opens a directory selection dialog and returns the path chosen by the user. diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index 00f11e1af8..6b32deecc3 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.3 +version: 0.9.4 environment: sdk: ">=2.18.0 <4.0.0" @@ -25,11 +25,11 @@ flutter: dependencies: file_selector_ios: ^0.5.0 - file_selector_linux: ^0.9.1 - file_selector_macos: ^0.9.1 - file_selector_platform_interface: ^2.3.0 - file_selector_web: ^0.9.0 - file_selector_windows: ^0.9.2 + file_selector_linux: ^0.9.2 + file_selector_macos: ^0.9.3 + file_selector_platform_interface: ^2.6.0 + file_selector_web: ^0.9.1 + file_selector_windows: ^0.9.3 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart index cdcebe0782..a9d684c0df 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -144,7 +144,80 @@ void main() { }); }); - group('getSavePath', () { + group('getSaveLocation', () { + const String expectedSavePath = '/example/path'; + + test('works', () async { + const int expectedActiveFilter = 1; + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName) + ..setPathsResponse([expectedSavePath], + activeFilter: expectedActiveFilter); + + final FileSaveLocation? location = await getSaveLocation( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName, + ); + + expect(location?.path, expectedSavePath); + expect(location?.activeFilter, acceptedTypeGroups[expectedActiveFilter]); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathsResponse([expectedSavePath]); + + final FileSaveLocation? location = await getSaveLocation(); + expect(location?.path, expectedSavePath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathsResponse([expectedSavePath]); + + final FileSaveLocation? location = + await getSaveLocation(initialDirectory: initialDirectory); + expect(location?.path, expectedSavePath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathsResponse([expectedSavePath]); + + final FileSaveLocation? location = + await getSaveLocation(confirmButtonText: confirmButtonText); + expect(location?.path, expectedSavePath); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setPathsResponse([expectedSavePath]); + + final FileSaveLocation? location = + await getSaveLocation(acceptedTypeGroups: acceptedTypeGroups); + expect(location?.path, expectedSavePath); + }); + + test('sets the suggested name', () async { + fakePlatformImplementation + ..setExpectations(suggestedName: suggestedName) + ..setPathsResponse([expectedSavePath]); + + final FileSaveLocation? location = + await getSaveLocation(suggestedName: suggestedName); + expect(location?.path, expectedSavePath); + }); + }); + + group('getSavePath (deprecated)', () { const String expectedSavePath = '/example/path'; test('works', () async { @@ -321,6 +394,7 @@ class FakeFileSelector extends Fake // Return values. List? files; List? paths; + int? activeFilter; void setExpectations({ List acceptedTypeGroups = const [], @@ -339,9 +413,9 @@ class FakeFileSelector extends Fake this.files = files; } - // ignore: use_setters_to_change_properties - void setPathsResponse(List paths) { + void setPathsResponse(List paths, {int? activeFilter}) { this.paths = paths; + this.activeFilter = activeFilter; } @override @@ -374,12 +448,35 @@ class FakeFileSelector extends Fake String? initialDirectory, String? suggestedName, String? confirmButtonText, + }) async { + final FileSaveLocation? result = await getSaveLocation( + acceptedTypeGroups: acceptedTypeGroups, + options: SaveDialogOptions( + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText, + ), + ); + return result?.path; + } + + @override + Future getSaveLocation({ + List? acceptedTypeGroups, + SaveDialogOptions options = const SaveDialogOptions(), }) async { expect(acceptedTypeGroups, this.acceptedTypeGroups); - expect(initialDirectory, this.initialDirectory); - expect(suggestedName, this.suggestedName); - expect(confirmButtonText, this.confirmButtonText); - return paths?[0]; + expect(options.initialDirectory, initialDirectory); + expect(options.suggestedName, suggestedName); + expect(options.confirmButtonText, confirmButtonText); + final String? path = paths?[0]; + final int? activeFilterIndex = activeFilter; + return path == null + ? null + : FileSaveLocation(path, + activeFilter: activeFilterIndex == null + ? null + : acceptedTypeGroups?[activeFilterIndex]); } @override