Adds desktop support (#271) and onFileLoading callback to web (#766)

This commit is contained in:
Miguel Ruivo 2021-08-10 17:54:23 +01:00
parent fc8fd7ceee
commit 660855c2ec
20 changed files with 830 additions and 32 deletions

View File

@ -1,3 +1,15 @@
## 4.0.0
### Desktop support added for all platforms (MacOS, Linux & Windows) ([#271](https://github.com/miguelpruivo/flutter_file_picker/issues/271)) 🎉
From now on, you'll be able to use file_picker with all your platforms, a big thanks to @philenius, which made this possible and allowed the [flutter_file_picker_desktop](https://github.com/philenius/flutter_file_picker_desktop) to be merged with this one.
Have in mind that because of platforms differences, that the following API methods aren't available to use on Desktop:
- The `onFileLoading()` isn't necessary, hence, `FilePickerStatus` won't change, since it hasn't any effect on those;
- `clearTemporaryFiles()` isn't necessary since those files aren't created — the platforms will always use a reference to the original file;
- There is a new optional parameter `dialogTitle` which can be used to set the title of the modal dialog when picking the files;
##### Web
Adds `onFileLoading()` to Web. ([#766](https://github.com/miguelpruivo/flutter_file_picker/issues/766)).
## 3.0.4
##### Android
- Addresses an issue where an invalid custom file extension wouldn't throw an error when it should. Thank you @Jahn08.

View File

@ -12,6 +12,9 @@
<a href="https://www.buymeacoffee.com/gQyz2MR">
<img alt="Buy me a coffee" src="https://img.shields.io/badge/Donate-Buy%20Me%20A%20Coffee-yellow.svg">
</a>
<a href="https://github.com/miguelpruivo/flutter_file_picker/issues"><img src="https://img.shields.io/github/issues/miguelpruivo/flutter_file_picker">
</a>
<img src="https://img.shields.io/github/license/miguelpruivo/flutter_file_picker">
</p>
# File Picker
@ -19,13 +22,12 @@ A package that allows you to use the native file explorer to pick single or mult
## Currently supported features
* Uses OS default native pickers
* Supports multiple platforms (Mobile, Web, Desktop and Flutter GO)
* Pick files using **custom format** filtering — you can provide a list of file extensions (pdf, svg, zip, etc.)
* Pick files from **cloud files** (GDrive, Dropbox, iCloud)
* Single or multiple file picks
* Different default type filtering (media, image, video, audio or any)
* Picking directories
* Flutter Web
* Desktop (MacOS, Linux and Windows through Flutter Go)
* Picking directories
* Load file data immediately into memory (`Uint8List`) if needed;
If you have any feature that you want to see in this package, please feel free to issue a suggestion. 🎉
@ -110,8 +112,16 @@ if (result != null) {
For full usage details refer to the **[Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** above.
## Example App
![Demo](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/example.gif)
![DemoMultiFilters](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/example_ios.gif)
#### Android
![Demo](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/screenshots/example.gif)
#### iOS
![DemoMultiFilters](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/screenshots/example_ios.gif)
#### MacOS
![DemoMacOS](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/screenshots/example_macos.png)
#### Linux
![DemoLinux](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/screenshots/example_linux.png)
#### Windows
![DemoWindows](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/screenshots/example_windows.png)
## Getting Started

View File

@ -4,7 +4,7 @@
// ignore_for_file: lines_longer_than_80_chars
import 'package:file_picker/_internal/file_picker_web.dart';
import 'package:file_picker/src/file_picker_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

View File

@ -4,5 +4,5 @@ import 'package:flutter/widgets.dart';
void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
runApp(new FilePickerDemo());
runApp(FilePickerDemo());
}

View File

@ -31,6 +31,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
_paths = (await FilePicker.platform.pickFiles(
type: _pickingType,
allowMultiple: _multiPick,
onFileLoading: (FilePickerStatus status) => print(status),
allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '').split(',')
: null,
@ -44,7 +45,6 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
if (!mounted) return;
setState(() {
_loadingPath = false;
print(_paths!.first.extension);
_fileName =
_paths != null ? _paths!.map((e) => e.name).toString() : '...';
});

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -1,11 +1,17 @@
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:file_picker/src/file_picker_io.dart';
import 'package:file_picker/src/file_picker_linux.dart';
import 'package:file_picker/src/file_picker_macos.dart';
import 'package:file_picker/src/windows/stub.dart'
if (dart.library.io) 'package:file_picker/src/windows/file_picker_windows.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'file_picker_io.dart';
import 'file_picker_result.dart';
const String defaultDialogTitle = 'File Picker';
enum FileType {
any,
media,
@ -32,7 +38,7 @@ abstract class FilePicker extends PlatformInterface {
static final Object _token = Object();
static FilePicker _instance = FilePickerIO();
static late FilePicker _instance = _setPlatform();
static FilePicker get platform => _instance;
@ -41,33 +47,53 @@ abstract class FilePicker extends PlatformInterface {
_instance = instance;
}
static FilePicker _setPlatform() {
if (Platform.isAndroid || Platform.isIOS) {
return FilePickerIO();
} else if (Platform.isLinux) {
return FilePickerLinux();
} else if (Platform.isWindows) {
return filePickerWithFFI();
} else if (Platform.isMacOS) {
return FilePickerMacOS();
} else {
throw UnimplementedError(
'The current platform "${Platform.operatingSystem}" is not supported by this plugin.',
);
}
}
/// Retrieves the file(s) from the underlying platform
///
/// Default [type] set to [FileType.any] with [allowMultiple] set to [false]
/// Optionally, [allowedExtensions] might be provided (e.g. `[pdf, svg, jpg]`.).
/// Default `type` set to [FileType.any] with `allowMultiple` set to `false`.
/// Optionally, `allowedExtensions` might be provided (e.g. `[pdf, svg, jpg]`.).
///
/// If [withData] is set, picked files will have its byte data immediately available on memory as `Uint8List`
/// If `withData` is set, picked files will have its byte data immediately available on memory as `Uint8List`
/// which can be useful if you are picking it for server upload or similar. However, have in mind that
/// enabling this on IO (iOS & Android) may result in out of memory issues if you allow multiple picks or
/// pick huge files. Use [withReadStream] instead. Defaults to `true` on web, `false` otherwise.
/// pick huge files. Use `withReadStream` instead. Defaults to `true` on web, `false` otherwise.
///
/// If [withReadStream] is set, picked files will have its byte data available as a [Stream<List<int>>]
/// If `withReadStream` is set, picked files will have its byte data available as a `Stream<List<int>>`
/// which can be useful for uploading and processing large files. Defaults to `false`.
///
/// If you want to track picking status, for example, because some files may take some time to be
/// cached (particularly those picked from cloud providers), you may want to set [onFileLoading] handler
/// that will give you the current status of picking.
///
/// If [allowCompression] is set, it will allow media to apply the default OS compression.
/// If `allowCompression` is set, it will allow media to apply the default OS compression.
/// Defaults to `true`.
///
/// The result is wrapped in a [FilePickerResult] which contains helper getters
/// with useful information regarding the picked [List<PlatformFile>].
/// `dialogTitle` can be optionally set on desktop platforms to set the modal window title. It will be ignored on
/// other platforms.
///
/// The result is wrapped in a `FilePickerResult` which contains helper getters
/// with useful information regarding the picked `List<PlatformFile>`.
///
/// For more information, check the [API documentation](https://github.com/miguelpruivo/flutter_file_picker/wiki/api).
///
/// Returns [null] if aborted.
/// Returns `null` if aborted.
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
@ -84,16 +110,20 @@ abstract class FilePicker extends PlatformInterface {
/// each platform and it isn't required to invoke this as the system should take care
/// of it whenever needed. However, this will force the cleanup if you want to manage those on your own.
///
/// Returns [true] if the files were removed with success, [false] otherwise.
/// This method is only available on mobile platforms (Android & iOS).
///
/// Returns `true` if the files were removed with success, `false` otherwise.
Future<bool?> clearTemporaryFiles() async => throw UnimplementedError(
'clearTemporaryFiles() has not been implemented.');
/// Selects a directory and returns its absolute path.
///
/// On Android, this requires to be running on SDK 21 or above, else won't work.
/// Returns [null] if folder path couldn't be resolved.
/// Returns `null` if folder path couldn't be resolved.
///
/// `dialogTitle` can be set to display a custom title on desktop platforms. It will be ignored on Web & IO.
///
/// Note: Some Android paths are protected, hence can't be accessed and will return `/` instead.
Future<String?> getDirectoryPath() async =>
Future<String?> getDirectoryPath({String? dialogTitle}) async =>
throw UnimplementedError('getDirectoryPath() has not been implemented.');
}

View File

@ -6,8 +6,6 @@ import 'package:file_picker/src/platform_file.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'file_picker_result.dart';
final MethodChannel _channel = MethodChannel(
'miguelruivo.flutter.plugins.filepicker',
Platform.isLinux || Platform.isWindows || Platform.isMacOS
@ -27,6 +25,7 @@ class FilePickerIO extends FilePicker {
Future<FilePickerResult?> pickFiles({
FileType type = FileType.any,
List<String>? allowedExtensions,
String? dialogTitle,
Function(FilePickerStatus)? onFileLoading,
bool? allowCompression = true,
bool allowMultiple = false,
@ -48,7 +47,7 @@ class FilePickerIO extends FilePicker {
_channel.invokeMethod<bool>('clear');
@override
Future<String?> getDirectoryPath() async {
Future<String?> getDirectoryPath({String? dialogTitle}) async {
try {
return await _channel.invokeMethod('dir', {});
} on PlatformException catch (ex) {

View File

@ -0,0 +1,128 @@
import 'dart:async';
import 'package:file_picker/src/file_picker.dart';
import 'package:file_picker/src/file_picker_result.dart';
import 'package:file_picker/src/platform_file.dart';
import 'package:file_picker/src/utils.dart';
class FilePickerLinux extends FilePicker {
@override
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool allowMultiple = false,
bool withData = false,
bool withReadStream = false,
}) async {
final String executable = await _getPathToExecutable();
final String fileFilter = fileTypeToFileFilter(
type,
allowedExtensions,
);
final List<String> arguments = generateCommandLineArguments(
dialogTitle ?? defaultDialogTitle,
fileFilter: fileFilter,
multipleFiles: allowMultiple,
pickDirectory: false,
);
final String? fileSelectionResult = await runExecutableWithArguments(
executable,
arguments,
);
if (fileSelectionResult == null) {
return null;
}
final List<String> filePaths = resultStringToFilePaths(
fileSelectionResult,
);
final List<PlatformFile> platformFiles = await filePathsToPlatformFiles(
filePaths,
withReadStream,
withData,
);
return FilePickerResult(platformFiles);
}
@override
Future<String?> getDirectoryPath({
String? dialogTitle,
}) async {
final executable = await _getPathToExecutable();
final arguments = generateCommandLineArguments(
dialogTitle ?? defaultDialogTitle,
pickDirectory: true,
);
return await runExecutableWithArguments(executable, arguments);
}
/// Returns the path to the executables `qarma` or `zenity` as a [String].
///
/// On Linux, the CLI tools `qarma` or `zenity` can be used to open a native
/// file picker dialog. It seems as if all Linux distributions have at least
/// one of these two tools pre-installed (on Ubuntu `zenity` is pre-installed).
/// The future returns an error, if neither of both executables was found on
/// the path.
Future<String> _getPathToExecutable() async {
try {
return await isExecutableOnPath('qarma');
} on Exception {
return await isExecutableOnPath('zenity');
}
}
String fileTypeToFileFilter(FileType type, List<String>? allowedExtensions) {
switch (type) {
case FileType.any:
return '*.*';
case FileType.audio:
return '*.mp3 *.wav *.midi *.ogg *.aac';
case FileType.custom:
return '*.' + allowedExtensions!.join(' *.');
case FileType.image:
return '*.bmp *.gif *.jpg *.jpeg *.png';
case FileType.media:
return '*.webm *.mpeg *.mkv *.mp4 *.avi *.mov *.flv *.jpg *.jpeg *.bmp *.gif *.png';
case FileType.video:
return '*.webm *.mpeg *.mkv *.mp4 *.avi *.mov *.flv';
default:
throw Exception('unknown file type');
}
}
List<String> generateCommandLineArguments(
String dialogTitle, {
String fileFilter = '',
bool multipleFiles = false,
bool pickDirectory = false,
}) {
final arguments = ['--file-selection', '--title', dialogTitle];
if (fileFilter.isNotEmpty) {
arguments.add('--file-filter=$fileFilter');
}
if (multipleFiles) {
arguments.add('--multiple');
}
if (pickDirectory) {
arguments.add('--directory');
}
return arguments;
}
/// Transforms the result string (stdout) of `qarma` / `zenity` into a [List]
/// of file paths.
List<String> resultStringToFilePaths(String fileSelectionResult) {
if (fileSelectionResult.trim().isEmpty) {
return [];
}
return fileSelectionResult.split('|');
}
}

View File

@ -0,0 +1,141 @@
import 'package:file_picker/file_picker.dart';
import 'package:file_picker/src/utils.dart';
class FilePickerMacOS extends FilePicker {
@override
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool allowMultiple = false,
bool withData = false,
bool withReadStream = false,
}) async {
final String executable = await isExecutableOnPath('osascript');
final String fileFilter = fileTypeToFileFilter(
type,
allowedExtensions,
);
final List<String> arguments = generateCommandLineArguments(
escapeDialogTitle(dialogTitle ?? defaultDialogTitle),
fileFilter: fileFilter,
multipleFiles: allowMultiple,
pickDirectory: false,
);
final String? fileSelectionResult = await runExecutableWithArguments(
executable,
arguments,
);
if (fileSelectionResult == null) {
return null;
}
final List<String> filePaths = resultStringToFilePaths(
fileSelectionResult,
);
final List<PlatformFile> platformFiles = await filePathsToPlatformFiles(
filePaths,
withReadStream,
withData,
);
return FilePickerResult(platformFiles);
}
@override
Future<String?> getDirectoryPath({
String? dialogTitle,
}) async {
final String executable = await isExecutableOnPath('osascript');
final List<String> arguments = generateCommandLineArguments(
escapeDialogTitle(dialogTitle ?? defaultDialogTitle),
pickDirectory: true,
);
final String? directorySelectionResult = await runExecutableWithArguments(
executable,
arguments,
);
if (directorySelectionResult == null) {
return null;
}
return resultStringToFilePaths(directorySelectionResult).first;
}
String fileTypeToFileFilter(FileType type, List<String>? allowedExtensions) {
switch (type) {
case FileType.any:
return '';
case FileType.audio:
return '"", "mp3", "wav", "midi", "ogg", "aac"';
case FileType.custom:
return '"", "' + allowedExtensions!.join('", "') + '"';
case FileType.image:
return '"", "jpg", "jpeg", "bmp", "gif", "png"';
case FileType.media:
return '"", "webm", "mpeg", "mkv", "mp4", "avi", "mov", "flv", "jpg", "jpeg", "bmp", "gif", "png"';
case FileType.video:
return '"", "webm", "mpeg", "mkv", "mp4", "avi", "mov", "flv"';
default:
throw Exception('unknown file type');
}
}
List<String> generateCommandLineArguments(
String dialogTitle, {
String fileFilter = '',
bool multipleFiles = false,
bool pickDirectory = false,
}) {
final arguments = ['-e'];
String argument = 'choose ';
if (pickDirectory) {
argument += 'folder ';
} else {
argument += 'file of type {$fileFilter} ';
if (multipleFiles) {
argument += 'with multiple selections allowed ';
}
}
argument += 'with prompt "$dialogTitle"';
arguments.add(argument);
return arguments;
}
String escapeDialogTitle(String dialogTitle) => dialogTitle
.replaceAll('\\', '\\\\')
.replaceAll('"', '\\"')
.replaceAll('\n', '\\\n');
/// Transforms the result string (stdout) of `osascript` into a [List] of
/// file paths.
List<String> resultStringToFilePaths(String fileSelectionResult) {
if (fileSelectionResult.trim().isEmpty) {
return [];
}
return fileSelectionResult
.trim()
.split(', ')
.map((String path) => path.trim())
.where((String path) => path.isNotEmpty)
.map((String path) {
final pathElements = path.split(':').where((e) => e.isNotEmpty).toList();
final alias = pathElements[0];
if (alias == 'alias macOS') {
return '/' + pathElements.sublist(1).join('/');
}
final volumeName = alias.substring(6);
return ['/Volumes', volumeName, ...pathElements.sublist(1)].join('/');
}).toList();
}
}

View File

@ -5,9 +5,6 @@ import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import '../src/file_picker_result.dart';
import '../src/platform_file.dart';
class FilePickerWeb extends FilePicker {
late Element _target;
final String _kFilePickerInputsDomId = '__file_picker_web-file-input';
@ -39,6 +36,7 @@ class FilePickerWeb extends FilePicker {
@override
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
FileType type = FileType.any,
List<String>? allowedExtensions,
bool allowMultiple = false,
@ -62,6 +60,11 @@ class FilePickerWeb extends FilePicker {
uploadInput.accept = accept;
bool changeEventTriggered = false;
if (onFileLoading != null) {
onFileLoading(FilePickerStatus.picking);
}
void changeEventListener(e) {
if (changeEventTriggered) {
return;
@ -86,6 +89,9 @@ class FilePickerWeb extends FilePicker {
));
if (pickedFiles.length >= files.length) {
if (onFileLoading != null) {
onFileLoading(FilePickerStatus.done);
}
filesCompleter.complete(pickedFiles);
}
}
@ -138,7 +144,7 @@ class FilePickerWeb extends FilePicker {
_target.children.add(uploadInput);
uploadInput.click();
final files = await filesCompleter.future;
final List<PlatformFile>? files = await filesCompleter.future;
return files == null ? null : FilePickerResult(files);
}

65
lib/src/utils.dart Normal file
View File

@ -0,0 +1,65 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart';
Future<List<PlatformFile>> filePathsToPlatformFiles(
List<String> filePaths,
bool withReadStream,
bool withData,
) {
return Future.wait(
filePaths
.where((String filePath) => filePath.isNotEmpty)
.map((String filePath) async {
final file = File(filePath);
if (withReadStream) {
return createPlatformFile(file, null, file.openRead());
}
if (!withData) {
return createPlatformFile(file, null, null);
}
final bytes = await file.readAsBytes();
return createPlatformFile(file, bytes, null);
}).toList(),
);
}
Future<PlatformFile> createPlatformFile(
File file,
Uint8List? bytes,
Stream<List<int>>? readStream,
) async =>
PlatformFile(
bytes: bytes,
name: basename(file.path),
path: file.path,
readStream: readStream,
size: await file.length(),
);
Future<String?> runExecutableWithArguments(
String executable,
List<String> arguments,
) async {
final processResult = await Process.run(executable, arguments);
final path = processResult.stdout?.toString().trim();
if (processResult.exitCode != 0 || path == null || path.isEmpty) {
return null;
}
return path;
}
Future<String> isExecutableOnPath(String executable) async {
final path = await runExecutableWithArguments('which', [executable]);
if (path == null) {
throw Exception(
'Couldn\'t find the executable $executable in the path.',
);
}
return path;
}

View File

@ -0,0 +1,189 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_picker/src/utils.dart';
import 'package:file_picker/src/windows/file_picker_windows_ffi_types.dart';
import 'package:path/path.dart';
FilePicker filePickerWithFFI() => FilePickerWindows();
class FilePickerWindows extends FilePicker {
@override
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool allowMultiple = false,
bool withData = false,
bool withReadStream = false,
}) async {
final comdlg32 = DynamicLibrary.open('comdlg32.dll');
final getOpenFileNameW =
comdlg32.lookupFunction<GetOpenFileNameW, GetOpenFileNameWDart>(
'GetOpenFileNameW');
final Pointer<OPENFILENAMEW> openFileName = calloc<OPENFILENAMEW>();
openFileName.ref.lStructSize = sizeOf<OPENFILENAMEW>();
openFileName.ref.lpstrTitle =
(dialogTitle ?? defaultDialogTitle).toNativeUtf16();
openFileName.ref.lpstrFile = calloc.allocate<Utf16>(maxPath);
openFileName.ref.lpstrFilter =
fileTypeToFileFilter(type, allowedExtensions).toNativeUtf16();
openFileName.ref.nMaxFile = maxPath;
openFileName.ref.lpstrInitialDir = ''.toNativeUtf16();
openFileName.ref.flags = ofnExplorer | ofnFileMustExist | ofnHideReadOnly;
if (allowMultiple) {
openFileName.ref.flags |= ofnAllowMultiSelect;
}
final result = getOpenFileNameW(openFileName);
if (result == 1) {
final filePaths =
_extractSelectedFilesFromOpenFileNameW(openFileName.ref);
final platformFiles =
await filePathsToPlatformFiles(filePaths, withReadStream, withData);
return FilePickerResult(platformFiles);
}
return null;
}
@override
Future<String?> getDirectoryPath({
String? dialogTitle,
}) {
final pathIdPointer = _pickDirectory(dialogTitle ?? defaultDialogTitle);
if (pathIdPointer == null) {
return Future.value(null);
}
return Future.value(
_getPathFromItemIdentifierList(pathIdPointer),
);
}
String fileTypeToFileFilter(FileType type, List<String>? allowedExtensions) {
switch (type) {
case FileType.any:
return '*.*\x00\x00';
case FileType.audio:
return 'Audios (*.mp3)\x00*.mp3\x00All Files (*.*)\x00*.*\x00\x00';
case FileType.custom:
return 'Files (*.${allowedExtensions!.join(',*.')})\x00\x00';
case FileType.image:
return 'Images (*.jpeg,*.png,*.gif)\x00*.jpg;*.jpeg;*.png;*.gif\x00All Files (*.*)\x00*.*\x00\x00';
case FileType.media:
return 'Videos (*.webm,*.wmv,*.mpeg,*.mkv,*.mp4,*.avi,*.mov,*.flv)\x00*.webm;*.wmv;*.mpeg;*.mkv;*mp4;*.avi;*.mov;*.flv\x00Images (*.jpeg,*.png,*.gif)\x00*.jpg;*.jpeg;*.png;*.gif\x00All Files (*.*)\x00*.*\x00\x00';
case FileType.video:
return 'Videos (*.webm,*.wmv,*.mpeg,*.mkv,*.mp4,*.avi,*.mov,*.flv)\x00*.webm;*.wmv;*.mpeg;*.mkv;*mp4;*.avi;*.mov;*.flv\x00All Files (*.*)\x00*.*\x00\x00';
default:
throw Exception('unknown file type');
}
}
/// Uses the Win32 API to display a dialog box that enables the user to select a folder.
///
/// Returns a PIDL that specifies the location of the selected folder relative to the root of the
/// namespace. Returns null, if the user clicked on the "Cancel" button in the dialog box.
Pointer? _pickDirectory(String dialogTitle) {
final shell32 = DynamicLibrary.open('shell32.dll');
final shBrowseForFolderW =
shell32.lookupFunction<SHBrowseForFolderW, SHBrowseForFolderW>(
'SHBrowseForFolderW');
final Pointer<BROWSEINFOA> browseInfo = calloc<BROWSEINFOA>();
browseInfo.ref.hwndOwner = nullptr;
browseInfo.ref.pidlRoot = nullptr;
browseInfo.ref.pszDisplayName = calloc.allocate<Utf16>(maxPath);
browseInfo.ref.lpszTitle = dialogTitle.toNativeUtf16();
browseInfo.ref.ulFlags =
bifEditBox | bifNewDialogStyle | bifReturnOnlyFsDirs;
final Pointer<NativeType> itemIdentifierList =
shBrowseForFolderW(browseInfo);
calloc.free(browseInfo.ref.pszDisplayName);
calloc.free(browseInfo.ref.lpszTitle);
calloc.free(browseInfo);
if (itemIdentifierList == nullptr) {
return null;
}
return itemIdentifierList;
}
/// Uses the Win32 API to convert an item identifier list to a file system path.
///
/// [lpItem] must contain the address of an item identifier list that specifies a
/// file or directory location relative to the root of the namespace (the desktop).
/// Returns the file system path as a [String]. Throws an exception, if the
/// conversion wasn't successful.
String _getPathFromItemIdentifierList(Pointer lpItem) {
final shell32 = DynamicLibrary.open('shell32.dll');
final shGetPathFromIDListW =
shell32.lookupFunction<SHGetPathFromIDListW, SHGetPathFromIDListWDart>(
'SHGetPathFromIDListW');
final Pointer<Utf16> pszPath = calloc.allocate<Utf16>(maxPath);
final int result = shGetPathFromIDListW(lpItem, pszPath);
if (result == 0x00000000) {
throw Exception(
'Failed to convert item identifier list to a file system path.');
}
calloc.free(pszPath);
return pszPath.toDartString();
}
/// Extracts the list of selected files from the Win32 API struct [OPENFILENAMEW].
///
/// After the user has closed the file picker dialog, Win32 API sets the property
/// [lpstrFile] of [OPENFILENAMEW] to the user's selection. This property contains
/// a string terminated by two [null] characters. If the user has selected only one
/// file, then the returned string contains the absolute file path, e. g.
/// `C:\Users\John\file1.jpg\x00\x00`. If the user has selected more than one file,
/// then the returned string contains the directory of the selected files, followed
/// by a [null] character, followed by the file names each separated by a [null]
/// character, e.g. `C:\Users\John\x00file1.jpg\x00file2.jpg\x00file3.jpg\x00\x00`.
List<String> _extractSelectedFilesFromOpenFileNameW(
OPENFILENAMEW openFileNameW,
) {
final List<String> filePaths = [];
final buffer = StringBuffer();
int i = 0;
bool lastCharWasNull = false;
while (true) {
final char = openFileNameW.lpstrFile.cast<Uint16>().elementAt(i).value;
if (char == 0) {
if (lastCharWasNull) {
break;
} else {
filePaths.add(buffer.toString());
buffer.clear();
lastCharWasNull = true;
}
} else {
lastCharWasNull = false;
buffer.writeCharCode(char);
}
i++;
}
if (filePaths.length > 1) {
final String directoryPath = filePaths.removeAt(0);
return filePaths
.map<String>((filePath) => join(directoryPath, filePath))
.toList();
}
return filePaths;
}
}

View File

@ -0,0 +1,204 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
/// Function from Win32 API to display a dialog box that enables the user to select a Shell folder.
///
/// Reference:
/// https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shbrowseforfolderw
typedef SHBrowseForFolderW = Pointer Function(
/// A pointer to a [BROWSEINFOA] structure that contains information used to display the dialog box.
Pointer lpbi,
);
/// Function from Win32 API to create an Open dialog box that lets the user specify the drive,
/// directory, and the name of a file or set of files to be opened.
///
/// Reference:
/// https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getopenfilenamew
typedef GetOpenFileNameW = Int8 Function(
/// A pointer to an [OPENFILENAMEW] structure that contains information used to initialize the
/// dialog box. When GetOpenFileName returns, this structure contains information about the user's
/// file selection.
Pointer unnamedParam1,
);
/// Dart equivalent of [GetOpenFileNameW].
typedef GetOpenFileNameWDart = int Function(
Pointer unnamedParam1,
);
/// Function from Win32 API to convert an item identifier list to a file system path.
///
/// Returns [true] if successful; otherwise, [false].
///
/// Reference:
/// https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetpathfromidlistw
typedef SHGetPathFromIDListW = Int8 Function(
/// The address of an item identifier list that specifies a file or directory location relative to
/// the root of the namespace (the desktop).
Pointer pidl,
/// The address of a buffer to receive the file system path. This buffer must be at least [maxPath]
/// characters in size.
Pointer<Utf16> pszPath,
);
/// Dart equivalent of [SHGetPathFromIDListW].
typedef SHGetPathFromIDListWDart = int Function(
Pointer pidl,
Pointer<Utf16> pszPath,
);
/// Struct from Win32 API that contains parameters for the [SHBrowseForFolderW] function and receives
/// information about the folder selected by the user.
///
/// Reference:
/// https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfoa
class BROWSEINFOA extends Struct {
/// A handle to the owner window for the dialog box.
external Pointer hwndOwner;
/// A PIDL that specifies the location of the root folder from which to start browsing. Only the
/// specified folder and its subfolders in the namespace hierarchy appear in the dialog box. This
/// member can be [null]; in that case, a default location is used.
external Pointer pidlRoot;
/// Pointer to a buffer to receive the display name of the folder selected by the user. The size
/// of this buffer is assumed to be [maxPath] characters.
external Pointer<Utf16> pszDisplayName;
/// Pointer to a null-terminated string that is displayed above the tree view control in the dialog
/// box. This string can be used to specify instructions to the user.
external Pointer lpszTitle;
/// Flags that specify the options for the dialog box. This member can be 0 or a combination of the
/// following values.
@Uint32()
external int ulFlags;
/// Pointer to an application-defined function that the dialog box calls when an event occurs. For
/// more information, see the BrowseCallbackProc function. This member can be [null].
external Pointer lpfn;
/// An application-defined value that the dialog box passes to the callback function, if one is
/// specified in [lpfn].
external Pointer lParam;
/// An [int] value that receives the index of the image associated with the selected folder, stored
/// in the system image list.
@Uint32()
external int iImage;
}
/// Struct from Win32 API that contains parameters for the [GetOpenFileNameW] function and receives
/// information about the file(s) selected by the user.
///
/// Reference:
/// https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew
class OPENFILENAMEW extends Struct {
/// The length, in bytes, of the structure. Use sizeof [OPENFILENAMEW] for this parameter.
@Uint32()
external int lStructSize;
/// A handle to the window that owns the dialog box. This member can be any valid window handle, or it can be [null] if the dialog box has no owner.
external Pointer hwndOwner;
/// If the OFN_ENABLETEMPLATEHANDLE flag is set in the Flags member, hInstance is a handle to a memory object containing a dialog box template. If the OFN_ENABLETEMPLATE flag is set, hInstance is a handle to a module that contains a dialog box template named by the lpTemplateName member. If neither flag is set, this member is ignored. If the OFN_EXPLORER flag is set, the system uses the specified template to create a dialog box that is a child of the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses the template to create an old-style dialog box that replaces the default dialog box.
external Pointer hInstance;
/// A buffer containing pairs of null-terminated filter strings. The last string in the buffer must be terminated by two [null] characters.
external Pointer<Utf16> lpstrFilter;
/// A static buffer that contains a pair of null-terminated filter strings for preserving the filter pattern chosen by the user.
external Pointer<Utf16> lpstrCustomFilter;
/// The size, in characters, of the buffer identified by [lpstrCustomFilter]. This buffer should be at least 40 characters long. This member is ignored if [lpstrCustomFilter] is [null] or points to a [null] string.
@Uint32()
external int nMaxCustFilter;
/// The index of the currently selected filter in the File Types control.
@Uint32()
external int nFilterIndex;
/// The file name used to initialize the File Name edit control. The first character of this buffer must be [null] if initialization is not necessary.
external Pointer<Utf16> lpstrFile;
/// The size, in characters, of the buffer pointed to by lpstrFile. The buffer must be large enough to store the path and file name string or strings, including the terminating [null] character. The GetOpenFileName and GetSaveFileName functions return [false] if the buffer is too small to contain the file information. The buffer should be at least 256 characters long.
@Uint32()
external int nMaxFile;
/// The file name and extension (without path information) of the selected file. This member can be [null].
external Pointer<Utf16> lpstrFileTitle;
/// The size, in characters, of the buffer pointed to by [lpstrFileTitle]. This member is ignored if [lpstrFileTitle] is [null].
@Uint32()
external int nMaxFileTitle;
/// The initial directory. The algorithm for selecting the initial directory varies on different platforms.
external Pointer<Utf16> lpstrInitialDir;
/// A string to be placed in the title bar of the dialog box. If this member is [null], the system uses the default title (that is, Save As or Open).
external Pointer<Utf16> lpstrTitle;
/// A set of bit flags you can use to initialize the dialog box. When the dialog box returns, it sets these flags to indicate the user's input.
@Uint32()
external int flags;
/// The zero-based offset, in characters, from the beginning of the path to the file name in the string pointed to by [lpstrFile].
@Uint16()
external int nFileOffset;
/// The zero-based offset, in characters, from the beginning of the path to the file name extension in the string pointed to by [lpstrFile].
@Uint16()
external int nFileExtension;
/// The default extension. GetOpenFileName and GetSaveFileName append this extension to the file name if the user fails to type an extension.
external Pointer<Utf16> lpstrDefExt;
/// Application-defined data that the system passes to the hook procedure identified by the lpfnHook member. When the system sends the WM_INITDIALOG message to the hook procedure, the message's lParam parameter is a pointer to the OPENFILENAME structure specified when the dialog box was created. The hook procedure can use this pointer to get the lCustData value.
external Pointer lCustData;
/// A pointer to a hook procedure. This member is ignored unless the Flags member includes the OFN_ENABLEHOOK flag.
external Pointer lpfnHook;
/// The name of the dialog template resource in the module identified by the hInstance member. For numbered dialog box resources, this can be a value returned by the MAKEINTRESOURCE macro. This member is ignored unless the OFN_ENABLETEMPLATE flag is set in the Flags member. If the OFN_EXPLORER flag is set, the system uses the specified template to create a dialog box that is a child of the default Explorer-style dialog box. If the OFN_EXPLORER flag is not set, the system uses the template to create an old-style dialog box that replaces the default dialog box.
external Pointer<Utf16> lpTemplateName;
/// This member is reserved.
external Pointer pvReserved;
/// This member is reserved.
@Uint32()
external int dwReserved;
/// A set of bit flags you can use to initialize the dialog box.
@Uint32()
external int flagsEx;
}
/// Only return file system directories. If the user selects folders that are not part of the file
/// system, the OK button is grayed.
const bifReturnOnlyFsDirs = 0x00000001;
/// Include an edit control in the browse dialog box that allows the user to type the name of an item.
const bifEditBox = 0x00000010;
/// Use the new user interface. Setting this flag provides the user with a larger dialog box that can
/// be resized. The dialog box has several new capabilities, including: drag-and-drop capability within
/// the dialog box, reordering, shortcut menus, new folders, delete, and other shortcut menu commands.
const bifNewDialogStyle = 0x00000040;
/// In the Windows API, the maximum length for a path is MAX_PATH, which is defined as 260 characters.
const maxPath = 260;
/// The File Name list box allows multiple selections.
const ofnAllowMultiSelect = 0x00000200;
/// Indicates that any customizations made to the Open or Save As dialog box use the Explorer-style customization methods.
const ofnExplorer = 0x00080000;
/// The user can type only names of existing files in the File Name entry field.
const ofnFileMustExist = 0x00001000;
/// Hides the Read Only check box.
const ofnHideReadOnly = 0x00000004;

View File

@ -0,0 +1,4 @@
import 'package:file_picker/file_picker.dart';
/// Stub method to support both dart:ffi and web
FilePicker filePickerWithFFI() => throw UnimplementedError('Unsupported');

View File

@ -1,7 +1,9 @@
name: file_picker
description: A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
version: 3.0.4
repository: https://github.com/miguelpruivo/flutter_file_picker
issue_tracker: https://github.com/miguelpruivo/flutter_file_picker/issues
version: 4.0.0
dependencies:
flutter:
@ -11,6 +13,8 @@ dependencies:
flutter_plugin_android_lifecycle: ^2.0.0
plugin_platform_interface: ^2.0.0
ffi: ^1.1.2
path: ^1.8.0
environment:
sdk: ">=2.12.0 <3.0.0"
@ -26,4 +30,10 @@ flutter:
pluginClass: FilePickerPlugin
web:
pluginClass: FilePickerWeb
fileName: _internal/file_picker_web.dart
fileName: src/file_picker_web.dart
macos:
default_package: file_picker
windows:
default_package: file_picker
linux:
default_package: file_picker