Skip to content

Commit

Permalink
Implement plugin functionality for iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Nov 7, 2021
1 parent adc7083 commit a269f83
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ dist

# TernJS port file
.tern-port

# Other
.DS_Store
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# cordova-plugin-save-dialog

This Cordova plugin for Android displays the native Save dialog which allows users to store a file in the selected location. The plugin utilizes the Storage Access Framework to save a file in a user-selected location as described in the [Android developer guide](https://developer.android.com/training/data-storage/shared/documents-files#create-file). Platforms other than Android are not supported currently.
This Cordova plugin displays the native Save dialog which allows users to store a file in the selected location.

In Android, the plugin utilizes the Storage Access Framework to save a file in a user-selected location as described in the [Android developer guide](https://developer.android.com/training/data-storage/shared/documents-files#create-file).

In iOS, the `UIDocumentPickerViewController`’s method [`initForExportingURLs:asCopy:`](https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller/3566731-initforexportingurls?language=objc) is used for opening a document picker that can export the file to the selected folder. Note that this method is only available in iOS 14.0+, so older iOS versions are not supported by the plugin.

## Installation

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
{
"name": "cordova-plugin-save-dialog",
"version": "0.1.1",
"description": "Cordova plugin for Android to display the native Save dialog and store a file in the selected location",
"version": "1.0.0",
"description": "Cordova plugin for opening the native Save dialog and storing a file in the user-selected location",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Amphiluke/cordova-plugin-save-dialog.git"
},
"keywords": [
"cordova",
"ecosystem:cordova",
"cordova-android",
"cordova-ios",
"file",
"save",
"dialog"
Expand Down
31 changes: 22 additions & 9 deletions plugin.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="cordova-plugin-save-dialog" version="0.1.1">
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="cordova-plugin-save-dialog" version="1.0.0">
<name>Save Dialog</name>
<description>Cordova plugin for Android to display the native Save dialog and store a file in the selected location</description>
<description>Cordova plugin for opening the native Save dialog and storing a file in the user-selected location</description>
<license>MIT</license>
<keywords>cordova,save,dialog</keywords>
<js-module src="www/SaveDialog.js" name="SaveDialog">
<clobbers target="cordova.plugins.saveDialog" />
</js-module>
<js-module src="www/BlobKeeper.js" name="BlobKeeper">
</js-module>

<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="SaveDialog" >
<param name="android-package" value="io.github.amphiluke.SaveDialog"/>
<feature name="SaveDialog">
<param name="android-package" value="io.github.amphiluke.SaveDialog" />
</feature>
</config-file>
<source-file src="src/android/SaveDialog.java" target-dir="src/io/github/amphiluke" />
<js-module src="www/android/SaveDialog.js" name="SaveDialog">
<clobbers target="cordova.plugins.saveDialog" />
</js-module>
<js-module src="www/android/BlobKeeper.js" name="BlobKeeper">
</js-module>
</platform>

<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="SaveDialog">
<param name="ios-package" value="CDVSaveDialog" />
</feature>
</config-file>
<header-file src="src/ios/CDVSaveDialog.h" />
<source-file src="src/ios/CDVSaveDialog.m" />
<js-module src="www/ios/SaveDialog.js" name="SaveDialog">
<clobbers target="cordova.plugins.saveDialog" />
</js-module>
</platform>
</plugin>
14 changes: 14 additions & 0 deletions src/ios/CDVSaveDialog.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#import <Cordova/CDVPlugin.h>
#import <UIKit/UIKit.h>

@interface CDVSaveDialog : CDVPlugin <UIDocumentPickerDelegate>

@property (nonatomic, retain) NSString* callbackId;

- (void)saveFile:(CDVInvokedUrlCommand*)command;
- (NSURL*)getPluginDirectory;
- (NSURL*)createTemporaryLocalFile:(NSData*)data fileName:(NSString*)name;
- (void)deleteTemporaryLocalFiles;
- (void)sendPluginResult:(BOOL)success message:(NSString*)message;

@end
83 changes: 83 additions & 0 deletions src/ios/CDVSaveDialog.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#import "CDVSaveDialog.h"
#import <Cordova/CDVPlugin.h>
#import <UIKit/UIKit.h>

@implementation CDVSaveDialog

- (void)saveFile:(CDVInvokedUrlCommand*)command
{
self.callbackId = command.callbackId;
if (@available(iOS 14, *)) {
NSData* data = [command.arguments objectAtIndex:0];
NSString* name = [command.arguments objectAtIndex:1];
NSURL* localFileUrl = [self createTemporaryLocalFile:data fileName:name];
if (localFileUrl == nil) {
[self sendPluginResult:NO message:@"Cannot create a temporary file"];
return;
}
NSArray* urls = @[localFileUrl];
UIDocumentPickerViewController* picker = [[UIDocumentPickerViewController alloc] initForExportingURLs:urls asCopy:YES];
picker.shouldShowFileExtensions = YES;
picker.delegate = self;
[self.viewController presentViewController:picker animated:YES completion:nil];
} else {
[self sendPluginResult:NO message:@"Unsupported iOS version"];
}
}

- (void)documentPicker:(UIDocumentPickerViewController*)picker didPickDocumentsAtURLs:(NSArray<NSURL*>*)urls
{
if ([urls count] > 0) {
[self sendPluginResult:YES message:nil];
} else {
[self sendPluginResult:NO message:@"Unknown error"];
}
[self deleteTemporaryLocalFiles];
}

- (void)documentPickerWasCancelled:(UIDocumentPickerViewController*)picker
{
[self sendPluginResult:NO message:@"The dialog has been cancelled"];
[self deleteTemporaryLocalFiles];
}

- (NSURL*)getPluginDirectory
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSURL* documentDir = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDir URLByAppendingPathComponent:@".SaveDialog" isDirectory:YES];
}

- (NSURL*)createTemporaryLocalFile:(NSData*)data fileName:(NSString*)name
{
NSURL* pluginDir = [self getPluginDirectory];
NSFileManager* fileManager = [NSFileManager defaultManager];
if (![fileManager createDirectoryAtURL:pluginDir withIntermediateDirectories:YES attributes:nil error:nil]) {
return nil;
}
NSURL* localFileUrl = [pluginDir URLByAppendingPathComponent:name isDirectory:NO];
if (![data writeToURL:localFileUrl atomically:YES]) {
return nil;
}
return localFileUrl;
}

- (void)deleteTemporaryLocalFiles
{
NSURL* pluginDir = [self getPluginDirectory];
NSFileManager* fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:pluginDir error:nil];
}

- (void)sendPluginResult:(BOOL)success message:(NSString*)message
{
CDVPluginResult* pluginResult = nil;
if (success) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message];
}
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
}

@end
File renamed without changes.
3 changes: 0 additions & 3 deletions www/SaveDialog.js → www/android/SaveDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ let saveFile = (uri, blob) => new Promise((resolve, reject) => {

module.exports = {
saveFile(blob, name = "") {
if (window.cordova.platformId !== "android") {
return Promise.reject("Unsupported platform");
}
return keepBlob(blob) // see the “resume” event handler below
.then(() => locateFile(blob.type, name))
.then(uri => saveFile(uri, blob))
Expand Down
23 changes: 23 additions & 0 deletions www/ios/SaveDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
let exec = require("cordova/exec");
let moduleMapper = require("cordova/modulemapper");
let FileReader = moduleMapper.getOriginalSymbol(window, "FileReader") || window.FileReader;

let saveFile = (blob, name) => new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
exec(resolve, reject, "SaveDialog", "saveFile", [reader.result, name]);
};
reader.onerror = () => {
reject(reader.error);
};
reader.onabort = () => {
reject("Blob reading has been aborted");
};
reader.readAsArrayBuffer(blob);
});

module.exports = {
saveFile(blob, name = "untitled") {
return saveFile(blob, name);
}
};

0 comments on commit a269f83

Please sign in to comment.