[file_selector] Add `getDirectoryPaths` (#3871)
Exposes the new `getDirectoryPaths` API, and updates the desktop implementation package constraints to ensure that the implementations are present.
This is a slightly updated recreation of
https://github.com/flutter/plugins/pull/6576
Fixes https://github.com/flutter/flutter/issues/74323
diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md
index ed6ce16..0111f0d 100644
--- a/packages/file_selector/file_selector/CHANGELOG.md
+++ b/packages/file_selector/file_selector/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.3
+
+* Adds `getDirectoryPaths` for selecting multiple directories.
+
## 0.9.2+5
* Updates references to the deprecated `macUTIs`.
diff --git a/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart
new file mode 100644
index 0000000..bdae92f
--- /dev/null
+++ b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart
@@ -0,0 +1,89 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector/file_selector.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select one or more directories using `getDirectoryPaths`,
+/// then displays the selected directories in a dialog.
+class GetMultipleDirectoriesPage extends StatelessWidget {
+ /// Returns a new instance of the page.
+ const GetMultipleDirectoriesPage({super.key});
+
+ Future<void> _getDirectoryPaths(BuildContext context) async {
+ const String confirmButtonText = 'Choose';
+ final List<String?> directoryPaths = await getDirectoryPaths(
+ confirmButtonText: confirmButtonText,
+ );
+ if (directoryPaths.isEmpty) {
+ // Operation was canceled by the user.
+ return;
+ }
+ String paths = '';
+ for (final String? path in directoryPaths) {
+ paths += '${path!} \n';
+ }
+ if (context.mounted) {
+ await showDialog<void>(
+ context: context,
+ builder: (BuildContext context) => TextDisplay(paths),
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Select multiple directories'),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: Colors.blue,
+ // ignore: deprecated_member_use
+ onPrimary: Colors.white,
+ ),
+ child: const Text(
+ 'Press to ask user to choose multiple directories'),
+ onPressed: () => _getDirectoryPaths(context),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+ /// Creates a `TextDisplay`.
+ const TextDisplay(this.directoriesPaths, {super.key});
+
+ /// The path selected in the dialog.
+ final String directoriesPaths;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: const Text('Selected Directories'),
+ content: Scrollbar(
+ child: SingleChildScrollView(
+ child: Text(directoriesPaths),
+ ),
+ ),
+ actions: <Widget>[
+ TextButton(
+ child: const Text('Close'),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart
index a532dc8..052dba0 100644
--- a/packages/file_selector/file_selector/example/lib/home_page.dart
+++ b/packages/file_selector/file_selector/example/lib/home_page.dart
@@ -55,6 +55,13 @@
child: const Text('Open a get directory dialog'),
onPressed: () => Navigator.pushNamed(context, '/directory'),
),
+ const SizedBox(height: 10),
+ ElevatedButton(
+ style: style,
+ child: const Text('Open a get multi directories dialog'),
+ onPressed: () =>
+ Navigator.pushNamed(context, '/multi-directories'),
+ ),
],
),
),
diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart
index 27b34e8..19ef8c0 100644
--- a/packages/file_selector/file_selector/example/lib/main.dart
+++ b/packages/file_selector/file_selector/example/lib/main.dart
@@ -5,6 +5,7 @@
import 'package:flutter/material.dart';
import 'get_directory_page.dart';
+import 'get_multiple_directories_page.dart';
import 'home_page.dart';
import 'open_image_page.dart';
import 'open_multiple_images_page.dart';
@@ -36,6 +37,8 @@
'/open/text': (BuildContext context) => const OpenTextPage(),
'/save/text': (BuildContext context) => SaveTextPage(),
'/directory': (BuildContext context) => GetDirectoryPage(),
+ '/multi-directories': (BuildContext context) =>
+ const GetMultipleDirectoriesPage()
},
);
}
diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart
index f357af0..c224956 100644
--- a/packages/file_selector/file_selector/lib/file_selector.dart
+++ b/packages/file_selector/file_selector/lib/file_selector.dart
@@ -106,6 +106,7 @@
}
/// Opens a directory selection dialog and returns the path chosen by the user.
+///
/// This always returns `null` on the web.
///
/// [initialDirectory] is the full path to the directory that will be displayed
@@ -123,3 +124,24 @@
return FileSelectorPlatform.instance.getDirectoryPath(
initialDirectory: initialDirectory, confirmButtonText: confirmButtonText);
}
+
+/// Opens a directory selection dialog and returns a list of the paths chosen
+/// by the user.
+///
+/// This always returns an empty array on the web.
+///
+/// [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.
+///
+/// [confirmButtonText] is the text in the confirmation button of the dialog.
+/// When not provided, the default OS label is used (for example, "Open").
+///
+/// Returns an empty array if the user cancels the operation.
+Future<List<String?>> getDirectoryPaths({
+ String? initialDirectory,
+ String? confirmButtonText,
+}) async {
+ return FileSelectorPlatform.instance.getDirectoryPaths(
+ initialDirectory: initialDirectory, confirmButtonText: confirmButtonText);
+}
diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml
index becf6d3..00f11e1 100644
--- a/packages/file_selector/file_selector/pubspec.yaml
+++ b/packages/file_selector/file_selector/pubspec.yaml
@@ -3,11 +3,11 @@
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.2+5
+version: 0.9.3
environment:
- sdk: ">=2.17.0 <4.0.0"
- flutter: ">=3.0.0"
+ sdk: ">=2.18.0 <4.0.0"
+ flutter: ">=3.3.0"
flutter:
plugin:
@@ -25,11 +25,11 @@
dependencies:
file_selector_ios: ^0.5.0
- file_selector_linux: ^0.9.0
- file_selector_macos: ^0.9.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.0
+ file_selector_windows: ^0.9.2
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 13c986b..cdcebe0 100644
--- a/packages/file_selector/file_selector/test/file_selector_test.dart
+++ b/packages/file_selector/file_selector/test/file_selector_test.dart
@@ -154,7 +154,7 @@
confirmButtonText: confirmButtonText,
acceptedTypeGroups: acceptedTypeGroups,
suggestedName: suggestedName)
- ..setPathResponse(expectedSavePath);
+ ..setPathsResponse(<String>[expectedSavePath]);
final String? savePath = await getSavePath(
initialDirectory: initialDirectory,
@@ -167,7 +167,7 @@
});
test('works with no arguments', () async {
- fakePlatformImplementation.setPathResponse(expectedSavePath);
+ fakePlatformImplementation.setPathsResponse(<String>[expectedSavePath]);
final String? savePath = await getSavePath();
expect(savePath, expectedSavePath);
@@ -176,7 +176,7 @@
test('sets the initial directory', () async {
fakePlatformImplementation
..setExpectations(initialDirectory: initialDirectory)
- ..setPathResponse(expectedSavePath);
+ ..setPathsResponse(<String>[expectedSavePath]);
final String? savePath =
await getSavePath(initialDirectory: initialDirectory);
@@ -186,7 +186,7 @@
test('sets the button confirmation label', () async {
fakePlatformImplementation
..setExpectations(confirmButtonText: confirmButtonText)
- ..setPathResponse(expectedSavePath);
+ ..setPathsResponse(<String>[expectedSavePath]);
final String? savePath =
await getSavePath(confirmButtonText: confirmButtonText);
@@ -196,7 +196,7 @@
test('sets the accepted type groups', () async {
fakePlatformImplementation
..setExpectations(acceptedTypeGroups: acceptedTypeGroups)
- ..setPathResponse(expectedSavePath);
+ ..setPathsResponse(<String>[expectedSavePath]);
final String? savePath =
await getSavePath(acceptedTypeGroups: acceptedTypeGroups);
@@ -206,7 +206,7 @@
test('sets the suggested name', () async {
fakePlatformImplementation
..setExpectations(suggestedName: suggestedName)
- ..setPathResponse(expectedSavePath);
+ ..setPathsResponse(<String>[expectedSavePath]);
final String? savePath = await getSavePath(suggestedName: suggestedName);
expect(savePath, expectedSavePath);
@@ -221,7 +221,7 @@
..setExpectations(
initialDirectory: initialDirectory,
confirmButtonText: confirmButtonText)
- ..setPathResponse(expectedDirectoryPath);
+ ..setPathsResponse(<String>[expectedDirectoryPath]);
final String? directoryPath = await getDirectoryPath(
initialDirectory: initialDirectory,
@@ -232,7 +232,8 @@
});
test('works with no arguments', () async {
- fakePlatformImplementation.setPathResponse(expectedDirectoryPath);
+ fakePlatformImplementation
+ .setPathsResponse(<String>[expectedDirectoryPath]);
final String? directoryPath = await getDirectoryPath();
expect(directoryPath, expectedDirectoryPath);
@@ -241,7 +242,7 @@
test('sets the initial directory', () async {
fakePlatformImplementation
..setExpectations(initialDirectory: initialDirectory)
- ..setPathResponse(expectedDirectoryPath);
+ ..setPathsResponse(<String>[expectedDirectoryPath]);
final String? directoryPath =
await getDirectoryPath(initialDirectory: initialDirectory);
@@ -251,13 +252,62 @@
test('sets the button confirmation label', () async {
fakePlatformImplementation
..setExpectations(confirmButtonText: confirmButtonText)
- ..setPathResponse(expectedDirectoryPath);
+ ..setPathsResponse(<String>[expectedDirectoryPath]);
final String? directoryPath =
await getDirectoryPath(confirmButtonText: confirmButtonText);
expect(directoryPath, expectedDirectoryPath);
});
});
+
+ group('getDirectoryPaths', () {
+ const List<String> expectedDirectoryPaths = <String>[
+ '/example/path',
+ '/example/2/path'
+ ];
+
+ test('works', () async {
+ fakePlatformImplementation
+ ..setExpectations(
+ initialDirectory: initialDirectory,
+ confirmButtonText: confirmButtonText)
+ ..setPathsResponse(expectedDirectoryPaths);
+
+ final List<String?> directoryPaths = await getDirectoryPaths(
+ initialDirectory: initialDirectory,
+ confirmButtonText: confirmButtonText,
+ );
+
+ expect(directoryPaths, expectedDirectoryPaths);
+ });
+
+ test('works with no arguments', () async {
+ fakePlatformImplementation.setPathsResponse(expectedDirectoryPaths);
+
+ final List<String?> directoryPaths = await getDirectoryPaths();
+ expect(directoryPaths, expectedDirectoryPaths);
+ });
+
+ test('sets the initial directory', () async {
+ fakePlatformImplementation
+ ..setExpectations(initialDirectory: initialDirectory)
+ ..setPathsResponse(expectedDirectoryPaths);
+
+ final List<String?> directoryPaths =
+ await getDirectoryPaths(initialDirectory: initialDirectory);
+ expect(directoryPaths, expectedDirectoryPaths);
+ });
+
+ test('sets the button confirmation label', () async {
+ fakePlatformImplementation
+ ..setExpectations(confirmButtonText: confirmButtonText)
+ ..setPathsResponse(expectedDirectoryPaths);
+
+ final List<String?> directoryPaths =
+ await getDirectoryPaths(confirmButtonText: confirmButtonText);
+ expect(directoryPaths, expectedDirectoryPaths);
+ });
+ });
}
class FakeFileSelector extends Fake
@@ -270,7 +320,7 @@
String? suggestedName;
// Return values.
List<XFile>? files;
- String? path;
+ List<String>? paths;
void setExpectations({
List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
@@ -290,8 +340,8 @@
}
// ignore: use_setters_to_change_properties
- void setPathResponse(String path) {
- this.path = path;
+ void setPathsResponse(List<String> paths) {
+ this.paths = paths;
}
@override
@@ -329,7 +379,7 @@
expect(initialDirectory, this.initialDirectory);
expect(suggestedName, this.suggestedName);
expect(confirmButtonText, this.confirmButtonText);
- return path;
+ return paths?[0];
}
@override
@@ -339,6 +389,16 @@
}) async {
expect(initialDirectory, this.initialDirectory);
expect(confirmButtonText, this.confirmButtonText);
- return path;
+ return paths?[0];
+ }
+
+ @override
+ Future<List<String>> getDirectoryPaths({
+ String? initialDirectory,
+ String? confirmButtonText,
+ }) async {
+ expect(initialDirectory, this.initialDirectory);
+ expect(confirmButtonText, this.confirmButtonText);
+ return paths!;
}
}