diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index ed6ce16bed3..0111f0d03c9 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 00000000000..bdae92fd6fa --- /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 _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List 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( + 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: [ + 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: [ + 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 a532dc84aab..052dba04333 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 @@ class HomePage extends StatelessWidget { 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 27b34e86fe1..19ef8c0242f 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 @@ class MyApp extends StatelessWidget { '/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 f357af07321..c2249565c9e 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 @@ Future getSavePath({ } /// 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 @@ Future getDirectoryPath({ 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> 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 becf6d39639..00f11e1af88 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -3,11 +3,11 @@ 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.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 @@ flutter: 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 13c986b0992..cdcebe07828 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 @@ void main() { confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, suggestedName: suggestedName) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath( initialDirectory: initialDirectory, @@ -167,7 +167,7 @@ void main() { }); test('works with no arguments', () async { - fakePlatformImplementation.setPathResponse(expectedSavePath); + fakePlatformImplementation.setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(); expect(savePath, expectedSavePath); @@ -176,7 +176,7 @@ void main() { test('sets the initial directory', () async { fakePlatformImplementation ..setExpectations(initialDirectory: initialDirectory) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(initialDirectory: initialDirectory); @@ -186,7 +186,7 @@ void main() { test('sets the button confirmation label', () async { fakePlatformImplementation ..setExpectations(confirmButtonText: confirmButtonText) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(confirmButtonText: confirmButtonText); @@ -196,7 +196,7 @@ void main() { test('sets the accepted type groups', () async { fakePlatformImplementation ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(acceptedTypeGroups: acceptedTypeGroups); @@ -206,7 +206,7 @@ void main() { test('sets the suggested name', () async { fakePlatformImplementation ..setExpectations(suggestedName: suggestedName) - ..setPathResponse(expectedSavePath); + ..setPathsResponse([expectedSavePath]); final String? savePath = await getSavePath(suggestedName: suggestedName); expect(savePath, expectedSavePath); @@ -221,7 +221,7 @@ void main() { ..setExpectations( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath( initialDirectory: initialDirectory, @@ -232,7 +232,8 @@ void main() { }); test('works with no arguments', () async { - fakePlatformImplementation.setPathResponse(expectedDirectoryPath); + fakePlatformImplementation + .setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(); expect(directoryPath, expectedDirectoryPath); @@ -241,7 +242,7 @@ void main() { test('sets the initial directory', () async { fakePlatformImplementation ..setExpectations(initialDirectory: initialDirectory) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(initialDirectory: initialDirectory); @@ -251,13 +252,62 @@ void main() { test('sets the button confirmation label', () async { fakePlatformImplementation ..setExpectations(confirmButtonText: confirmButtonText) - ..setPathResponse(expectedDirectoryPath); + ..setPathsResponse([expectedDirectoryPath]); final String? directoryPath = await getDirectoryPath(confirmButtonText: confirmButtonText); expect(directoryPath, expectedDirectoryPath); }); }); + + group('getDirectoryPaths', () { + const List expectedDirectoryPaths = [ + '/example/path', + '/example/2/path' + ]; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = await getDirectoryPaths( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + ); + + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = await getDirectoryPaths(); + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = + await getDirectoryPaths(initialDirectory: initialDirectory); + expect(directoryPaths, expectedDirectoryPaths); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathsResponse(expectedDirectoryPaths); + + final List directoryPaths = + await getDirectoryPaths(confirmButtonText: confirmButtonText); + expect(directoryPaths, expectedDirectoryPaths); + }); + }); } class FakeFileSelector extends Fake @@ -270,7 +320,7 @@ class FakeFileSelector extends Fake String? suggestedName; // Return values. List? files; - String? path; + List? paths; void setExpectations({ List acceptedTypeGroups = const [], @@ -290,8 +340,8 @@ class FakeFileSelector extends Fake } // ignore: use_setters_to_change_properties - void setPathResponse(String path) { - this.path = path; + void setPathsResponse(List paths) { + this.paths = paths; } @override @@ -329,7 +379,7 @@ class FakeFileSelector extends Fake expect(initialDirectory, this.initialDirectory); expect(suggestedName, this.suggestedName); expect(confirmButtonText, this.confirmButtonText); - return path; + return paths?[0]; } @override @@ -339,6 +389,16 @@ class FakeFileSelector extends Fake }) async { expect(initialDirectory, this.initialDirectory); expect(confirmButtonText, this.confirmButtonText); - return path; + return paths?[0]; + } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(initialDirectory, this.initialDirectory); + expect(confirmButtonText, this.confirmButtonText); + return paths!; } }