Skip to content

Commit

Permalink
[file_selector] Add file group to save return value (#4222)
Browse files Browse the repository at this point in the history
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 flutter/flutter#107093
  • Loading branch information
stuartmorgan committed Jun 23, 2023
1 parent 08425c5 commit 25e1d87
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/file_selector/file_selector/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
7 changes: 4 additions & 3 deletions packages/file_selector/file_selector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ final List<XFile> files = await openFiles(acceptedTypeGroups: <XTypeGroup>[
<?code-excerpt "readme_standalone_excerpts.dart (Save)"?>
```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;
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ class _MyAppState extends State<MyApp> {
Future<void> 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;
}
Expand All @@ -49,7 +50,7 @@ class _MyAppState extends State<MyApp> {
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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
Expand Down
50 changes: 42 additions & 8 deletions packages/file_selector/file_selector/lib/file_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -92,20 +92,54 @@ Future<List<XFile>> 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<String?> getSavePath({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
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<FileSaveLocation?> getSaveLocation({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
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.
Expand Down
12 changes: 6 additions & 6 deletions packages/file_selector/file_selector/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down
111 changes: 104 additions & 7 deletions packages/file_selector/file_selector/test/file_selector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(<String>[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(<String>[expectedSavePath]);

final FileSaveLocation? location = await getSaveLocation();
expect(location?.path, expectedSavePath);
});

test('sets the initial directory', () async {
fakePlatformImplementation
..setExpectations(initialDirectory: initialDirectory)
..setPathsResponse(<String>[expectedSavePath]);

final FileSaveLocation? location =
await getSaveLocation(initialDirectory: initialDirectory);
expect(location?.path, expectedSavePath);
});

test('sets the button confirmation label', () async {
fakePlatformImplementation
..setExpectations(confirmButtonText: confirmButtonText)
..setPathsResponse(<String>[expectedSavePath]);

final FileSaveLocation? location =
await getSaveLocation(confirmButtonText: confirmButtonText);
expect(location?.path, expectedSavePath);
});

test('sets the accepted type groups', () async {
fakePlatformImplementation
..setExpectations(acceptedTypeGroups: acceptedTypeGroups)
..setPathsResponse(<String>[expectedSavePath]);

final FileSaveLocation? location =
await getSaveLocation(acceptedTypeGroups: acceptedTypeGroups);
expect(location?.path, expectedSavePath);
});

test('sets the suggested name', () async {
fakePlatformImplementation
..setExpectations(suggestedName: suggestedName)
..setPathsResponse(<String>[expectedSavePath]);

final FileSaveLocation? location =
await getSaveLocation(suggestedName: suggestedName);
expect(location?.path, expectedSavePath);
});
});

group('getSavePath (deprecated)', () {
const String expectedSavePath = '/example/path';

test('works', () async {
Expand Down Expand Up @@ -321,6 +394,7 @@ class FakeFileSelector extends Fake
// Return values.
List<XFile>? files;
List<String>? paths;
int? activeFilter;

void setExpectations({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
Expand All @@ -339,9 +413,9 @@ class FakeFileSelector extends Fake
this.files = files;
}

// ignore: use_setters_to_change_properties
void setPathsResponse(List<String> paths) {
void setPathsResponse(List<String> paths, {int? activeFilter}) {
this.paths = paths;
this.activeFilter = activeFilter;
}

@override
Expand Down Expand Up @@ -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<FileSaveLocation?> getSaveLocation({
List<XTypeGroup>? 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
Expand Down

0 comments on commit 25e1d87

Please sign in to comment.