mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 15:23:25 +08:00
[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
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
## NEXT
|
## 0.9.4
|
||||||
|
|
||||||
|
* Adds `getSaveLocation` and deprecates `getSavePath`.
|
||||||
* Updates minimum supported macOS version to 10.14.
|
* Updates minimum supported macOS version to 10.14.
|
||||||
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
|
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
|
||||||
|
|
||||||
|
@ -68,8 +68,9 @@ final List<XFile> files = await openFiles(acceptedTypeGroups: <XTypeGroup>[
|
|||||||
<?code-excerpt "readme_standalone_excerpts.dart (Save)"?>
|
<?code-excerpt "readme_standalone_excerpts.dart (Save)"?>
|
||||||
```dart
|
```dart
|
||||||
const String fileName = 'suggested_name.txt';
|
const String fileName = 'suggested_name.txt';
|
||||||
final String? path = await getSavePath(suggestedName: fileName);
|
final FileSaveLocation? result =
|
||||||
if (path == null) {
|
await getSaveLocation(suggestedName: fileName);
|
||||||
|
if (result == null) {
|
||||||
// Operation was canceled by the user.
|
// Operation was canceled by the user.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -78,7 +79,7 @@ final Uint8List fileData = Uint8List.fromList('Hello World!'.codeUnits);
|
|||||||
const String mimeType = 'text/plain';
|
const String mimeType = 'text/plain';
|
||||||
final XFile textFile =
|
final XFile textFile =
|
||||||
XFile.fromData(fileData, mimeType: mimeType, name: fileName);
|
XFile.fromData(fileData, mimeType: mimeType, name: fileName);
|
||||||
await textFile.saveTo(path);
|
await textFile.saveTo(result.path);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get a directory path
|
#### Get a directory path
|
||||||
|
@ -39,8 +39,9 @@ class _MyAppState extends State<MyApp> {
|
|||||||
Future<void> saveFile() async {
|
Future<void> saveFile() async {
|
||||||
// #docregion Save
|
// #docregion Save
|
||||||
const String fileName = 'suggested_name.txt';
|
const String fileName = 'suggested_name.txt';
|
||||||
final String? path = await getSavePath(suggestedName: fileName);
|
final FileSaveLocation? result =
|
||||||
if (path == null) {
|
await getSaveLocation(suggestedName: fileName);
|
||||||
|
if (result == null) {
|
||||||
// Operation was canceled by the user.
|
// Operation was canceled by the user.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -49,7 +50,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
const String mimeType = 'text/plain';
|
const String mimeType = 'text/plain';
|
||||||
final XFile textFile =
|
final XFile textFile =
|
||||||
XFile.fromData(fileData, mimeType: mimeType, name: fileName);
|
XFile.fromData(fileData, mimeType: mimeType, name: fileName);
|
||||||
await textFile.saveTo(path);
|
await textFile.saveTo(result.path);
|
||||||
// #enddocregion Save
|
// #enddocregion Save
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ class SaveTextPage extends StatelessWidget {
|
|||||||
// file will be saved. In most cases, this parameter should not be provided.
|
// file will be saved. In most cases, this parameter should not be provided.
|
||||||
final String initialDirectory =
|
final String initialDirectory =
|
||||||
(await getApplicationDocumentsDirectory()).path;
|
(await getApplicationDocumentsDirectory()).path;
|
||||||
final String? path = await getSavePath(
|
final FileSaveLocation? result = await getSaveLocation(
|
||||||
initialDirectory: initialDirectory,
|
initialDirectory: initialDirectory,
|
||||||
suggestedName: fileName,
|
suggestedName: fileName,
|
||||||
);
|
);
|
||||||
if (path == null) {
|
if (result == null) {
|
||||||
// Operation was canceled by the user.
|
// Operation was canceled by the user.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ class SaveTextPage extends StatelessWidget {
|
|||||||
final XFile textFile =
|
final XFile textFile =
|
||||||
XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
|
XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
|
||||||
|
|
||||||
await textFile.saveTo(path);
|
await textFile.saveTo(result.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -7,7 +7,7 @@ import 'dart:async';
|
|||||||
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
||||||
|
|
||||||
export '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.
|
/// Opens a file selection dialog and returns the path chosen by the user.
|
||||||
///
|
///
|
||||||
@ -92,20 +92,54 @@ Future<List<XFile>> openFiles({
|
|||||||
/// When not provided, the default OS label is used (for example, "Save").
|
/// When not provided, the default OS label is used (for example, "Save").
|
||||||
///
|
///
|
||||||
/// Returns `null` if the user cancels the operation.
|
/// Returns `null` if the user cancels the operation.
|
||||||
|
@Deprecated('Use getSaveLocation instead')
|
||||||
Future<String?> getSavePath({
|
Future<String?> getSavePath({
|
||||||
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
|
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
|
||||||
String? initialDirectory,
|
String? initialDirectory,
|
||||||
String? suggestedName,
|
String? suggestedName,
|
||||||
String? confirmButtonText,
|
String? confirmButtonText,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO(stuartmorgan): Update this to getSaveLocation in the next federated
|
return (await getSaveLocation(
|
||||||
// change PR.
|
acceptedTypeGroups: acceptedTypeGroups,
|
||||||
// ignore: deprecated_member_use
|
initialDirectory: initialDirectory,
|
||||||
return FileSelectorPlatform.instance.getSavePath(
|
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,
|
acceptedTypeGroups: acceptedTypeGroups,
|
||||||
initialDirectory: initialDirectory,
|
options: SaveDialogOptions(
|
||||||
suggestedName: suggestedName,
|
initialDirectory: initialDirectory,
|
||||||
confirmButtonText: confirmButtonText);
|
suggestedName: suggestedName,
|
||||||
|
confirmButtonText: confirmButtonText));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a directory selection dialog and returns the path chosen by the user.
|
/// Opens a directory selection dialog and returns the path chosen by the user.
|
||||||
|
@ -3,7 +3,7 @@ description: Flutter plugin for opening and saving files, or selecting
|
|||||||
directories, using native file selection UI.
|
directories, using native file selection UI.
|
||||||
repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector
|
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
|
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:
|
environment:
|
||||||
sdk: ">=2.18.0 <4.0.0"
|
sdk: ">=2.18.0 <4.0.0"
|
||||||
@ -25,11 +25,11 @@ flutter:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
file_selector_ios: ^0.5.0
|
file_selector_ios: ^0.5.0
|
||||||
file_selector_linux: ^0.9.1
|
file_selector_linux: ^0.9.2
|
||||||
file_selector_macos: ^0.9.1
|
file_selector_macos: ^0.9.3
|
||||||
file_selector_platform_interface: ^2.3.0
|
file_selector_platform_interface: ^2.6.0
|
||||||
file_selector_web: ^0.9.0
|
file_selector_web: ^0.9.1
|
||||||
file_selector_windows: ^0.9.2
|
file_selector_windows: ^0.9.3
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
@ -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';
|
const String expectedSavePath = '/example/path';
|
||||||
|
|
||||||
test('works', () async {
|
test('works', () async {
|
||||||
@ -321,6 +394,7 @@ class FakeFileSelector extends Fake
|
|||||||
// Return values.
|
// Return values.
|
||||||
List<XFile>? files;
|
List<XFile>? files;
|
||||||
List<String>? paths;
|
List<String>? paths;
|
||||||
|
int? activeFilter;
|
||||||
|
|
||||||
void setExpectations({
|
void setExpectations({
|
||||||
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
|
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
|
||||||
@ -339,9 +413,9 @@ class FakeFileSelector extends Fake
|
|||||||
this.files = files;
|
this.files = files;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: use_setters_to_change_properties
|
void setPathsResponse(List<String> paths, {int? activeFilter}) {
|
||||||
void setPathsResponse(List<String> paths) {
|
|
||||||
this.paths = paths;
|
this.paths = paths;
|
||||||
|
this.activeFilter = activeFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -374,12 +448,35 @@ class FakeFileSelector extends Fake
|
|||||||
String? initialDirectory,
|
String? initialDirectory,
|
||||||
String? suggestedName,
|
String? suggestedName,
|
||||||
String? confirmButtonText,
|
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 {
|
}) async {
|
||||||
expect(acceptedTypeGroups, this.acceptedTypeGroups);
|
expect(acceptedTypeGroups, this.acceptedTypeGroups);
|
||||||
expect(initialDirectory, this.initialDirectory);
|
expect(options.initialDirectory, initialDirectory);
|
||||||
expect(suggestedName, this.suggestedName);
|
expect(options.suggestedName, suggestedName);
|
||||||
expect(confirmButtonText, this.confirmButtonText);
|
expect(options.confirmButtonText, confirmButtonText);
|
||||||
return paths?[0];
|
final String? path = paths?[0];
|
||||||
|
final int? activeFilterIndex = activeFilter;
|
||||||
|
return path == null
|
||||||
|
? null
|
||||||
|
: FileSaveLocation(path,
|
||||||
|
activeFilter: activeFilterIndex == null
|
||||||
|
? null
|
||||||
|
: acceptedTypeGroups?[activeFilterIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Reference in New Issue
Block a user