Skip to content

feature: pickFile() is now implemented for windows #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,11 +872,17 @@ For more information read the [Adding an App to an App Group](https://developer.
```ts
function pickFile(options?: PickFileOptionsT): Promise<string[]>;
```
**SUPPORTED**: Android, iOS, macOS. **NOT YET SUPPORTED**: Windows.
**SUPPORTED**: Android, iOS, macOS, Windows.

Prompts the user to select file(s) using a platform-provided file picker UI,
which also allows to access files outside the app sandbox.

**BEWARE:** On **windows** the options differ from the other platform since the
native file piper doesn't support `mimeTypes` but `fileExtensions` and different
picker types like `singleFile`, `multipleFIle` and `folder`. For more information
see [PickFileOptionsT].


**BEWARE:** On **macOS (Catalyst)** for this function to work you MUST go to
_Signing & Capabilities_ settings of your project, and inside its _App Sandbox_
section to set _File Access_ > _User Selected Files_ to _Read/Write_ value
Expand Down Expand Up @@ -1404,14 +1410,25 @@ Type of extra options argument for [mkdir()].
```ts
type PickFileOptionsT = {
mimeTypes?: string[];
pickerType?: 'singleFile' | 'multipleFiles' | 'folder';
fileExtensions?: FileExtension[]
};
```
Optional parameters for [pickFile()] function.

- `mimeTypes` &mdash; **string[]** &mdash; Optional. An array of
- `mimeTypes` &mdash; **string[]** &mdash; *[Android - iOS - macOS]* Optional. An array of
[MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
of files user is allowed to select. Defaults to `['*/*']` allowing to select
any file.
- `pickerType` &mdash; **'singleFile' | 'multipleFiles' | 'folder'** &mdash; *[Windows]* Optional.
The type of objects to pick can be either a single file, multiple files or one folder.
- Multiple folders are not supported by windows.
- Defaults to `'singleFile'` */
- `fileExtensions` &mdash; **FileExtension[]** &mdash; *[Windows]* Optional, The file extensions to pick from.
- Only applies to `pickerType !== 'folder'`
- Defaults to `[]` (all file extensions) */
- `FileExtension = ´.${string}´` - e.g `'.bmp'`, `'.mp3'`


### ReadDirAssetsResItemT
[ReadDirAssetsResItemT]: #readdirassetsresitemt
Expand Down
14 changes: 14 additions & 0 deletions src/NativeReactNativeFs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { TurboModuleRegistry } from 'react-native';
// Note: It would be better to have all these type definitions in a dedicated
// module, however as of its current version RN's Codegen does not seem to handle
// type imports correctly.
// TODO everything needs jsdoc comments. probably the same as in the README.md file.
// It's better to read jsdoc through intellisense than to open and find things in the README.md.

export type DownloadBeginCallbackResultT = {
jobId: number; // The download job ID, required if one wishes to cancel the download. See `stopDownload`.
Expand Down Expand Up @@ -40,8 +42,20 @@ export type NativeDownloadFileOptionsT = {
hasResumableCallback: boolean;
};

export type FileExtension = `.${string}`;
export type PickFileOptionsT = {
/** **[Android - iOS - macOS]** The mime types that restrict the type of files that can be picked.
* - For more information, see [Common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types). */
mimeTypes: string[];
/** **[Windows]**
* The type of objects to pick can be either a single file, multiple files or one folder.
* - Multiple folders are not supported by windows.
* - Defaults to `'singleFile'` */
pickerType: 'singleFile' | 'multipleFiles' | 'folder';
/** **[Windows]** The file extensions to pick from.
* - Only applies to `pickerType !== 'folder'`
* - Defaults to `[]` (all file extensions) */
fileExtensions: FileExtension[]
};

export type DownloadFileOptionsT = {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ export function pickFile(
): Promise<string[]> {
return RNFS.pickFile({
mimeTypes: options.mimeTypes || ['*/*'],
pickerType: options.pickerType || 'singleFile',
fileExtensions: options.fileExtensions || [],
});
}

Expand Down
161 changes: 161 additions & 0 deletions windows/ReactNativeFs/ReactNativeModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#include <winrt/Windows.Web.Http.Headers.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Storage.AccessCache.h>

#include "RNFSException.h"

Expand All @@ -24,6 +26,7 @@ using namespace winrt::ReactNativeFs;
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Storage::Streams;
using namespace winrt::Windows::Storage::Pickers;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Web::Http;

Expand Down Expand Up @@ -1127,6 +1130,164 @@ IAsyncAction ReactNativeModule::ProcessUploadRequestAsync(ReactPromise<JSValueOb
}
}

void ReactNativeModule::pickFile(JSValueObject options, ReactPromise<JSValueArray> promise) noexcept
{
m_reactContext.UIDispatcher().Post([this, options = std::move(options), promise = std::move(promise)]() mutable {
try
{
// read options
std::string pickerType = "multipleFiles"; // Default value
if (options.find("pickerType") != options.end())
{
pickerType = options["pickerType"].AsString();
}

std::vector<std::wstring> fileTypes;
if (options.find("fileExtensions") != options.end())
{
for (const auto& mimeType : options["fileExtensions"].AsArray())
{
fileTypes.push_back(std::wstring(winrt::to_hstring(mimeType.AsString())));
}
}

// folder picker
if (pickerType == "folder")
{
FolderPicker picker;
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);
picker.FileTypeFilter().Append(L"*");

picker.PickSingleFolderAsync().Completed([promise = std::move(promise)](IAsyncOperation<StorageFolder> const& operation, AsyncStatus const status) mutable {
try
{
if (status == AsyncStatus::Completed)
{
StorageFolder folder = operation.GetResults();
if (folder)
{
JSValueArray result;
result.push_back(JSValueObject{
{"name", winrt::to_string(folder.Name())},
{"path", winrt::to_string(folder.Path())}
});
promise.Resolve(std::move(result));
}
else
{
promise.Reject("No folder was picked.");
}
}
else
{
promise.Reject("Folder picker operation was not completed.");
}
}
catch (const hresult_error& ex)
{
promise.Reject(winrt::to_string(ex.message()).c_str());
}
});
}

// file picker
else
{
FileOpenPicker picker;
picker.ViewMode(PickerViewMode::Thumbnail);
picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary);

if (!fileTypes.empty())
{
for (const auto& fileType : fileTypes)
{
picker.FileTypeFilter().Append(fileType);
}
}
else
{
picker.FileTypeFilter().Append(L"*");
}

// single files
if (pickerType == "singleFile")
{
picker.PickSingleFileAsync().Completed([promise = std::move(promise)](IAsyncOperation<StorageFile> const& operation, AsyncStatus const status) mutable {
try
{
if (status == AsyncStatus::Completed)
{
StorageFile file = operation.GetResults();
if (file)
{
JSValueArray result;
result.push_back(winrt::to_string(file.Path()));
promise.Resolve(std::move(result));
}
else
{
promise.Reject("No file was picked.");
}
}
else
{
promise.Reject("File picker operation was not completed.");
}
}
catch (const hresult_error& ex)
{
promise.Reject(winrt::to_string(ex.message()).c_str());
}
});
}

// multiple files
else if (pickerType == "multipleFiles")
{
picker.PickMultipleFilesAsync().Completed([promise = std::move(promise)](IAsyncOperation<IVectorView<StorageFile>> const& operation, AsyncStatus const status) mutable {
try
{
if (status == AsyncStatus::Completed)
{
auto files = operation.GetResults();
if (files.Size() > 0)
{
JSValueArray result;
for (const auto& file : files)
{
result.push_back(winrt::to_string(file.Path()));
}
promise.Resolve(std::move(result));
}
else
{
promise.Reject("No files were picked.");
}
}
else
{
promise.Reject("File picker operation was not completed.");
}
}
catch (const hresult_error& ex)
{
promise.Reject(winrt::to_string(ex.message()).c_str());
}
});
}
else
{
promise.Reject("Invalid pickerType option.");
}
}
}
catch (const hresult_error& ex)
{
promise.Reject(winrt::to_string(ex.message()).c_str());
}
});
}

void ReactNativeModule::addListener(std::string eventName) noexcept
{
// Keep: Required for RN built in Event Emitter Calls.
Expand Down
3 changes: 3 additions & 0 deletions windows/ReactNativeFs/ReactNativeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ struct ReactNativeModule
REACT_METHOD(removeListeners);
void removeListeners(int count) noexcept;

REACT_METHOD(pickFile);
void pickFile(JSValueObject options, ReactPromise<JSValueArray> promise) noexcept;

private:
void splitPath(const std::wstring& fullPath, winrt::hstring& directoryPath, winrt::hstring& fileName) noexcept;

Expand Down