feat(file-system): append, appendText & createFile (#10285)

This commit is contained in:
Osei Fortune
2023-05-04 23:45:01 -04:00
committed by GitHub
parent 7bb0918e08
commit ab32aeaaa3
36 changed files with 2079 additions and 654 deletions

View File

@ -5,7 +5,7 @@ import * as fs from '@nativescript/core/file-system';
import * as TKUnit from '../tk-unit';
import * as appModule from '@nativescript/core/application';
import { isIOS, Device, platformNames } from '@nativescript/core';
import { isIOS, Device, platformNames, isAndroid } from '@nativescript/core';
export var testPathNormalize = function () {
// >> file-system-normalize
@ -719,3 +719,62 @@ export function test_FileCopy(done) {
.then(() => done())
.catch(done);
}
export function testAndroidCreate() {
let testFunc = function testFunc() {
const file = fs.File.android.createFile({
directory: fs.AndroidDirectory.DOWNLOADS,
name: `${Date.now()}.txt`,
mime: 'text/plain',
relativePath: `NativeScript`,
});
file.writeTextSync('some text');
return file;
};
if (isAndroid) {
const file = testFunc();
TKUnit.assertEqual(file.readTextSync(), 'some text', `The contents of the new file created in the 'AndroidDirectory.DOWNLOADS' folder are not as expected.`);
file.removeSync();
TKUnit.assertTrue(!fs.File.exists(file.path));
} else {
TKUnit.assertThrows(testFunc, `Trying to retrieve createFile on a platform different from Android should throw!`, `createFile is available on Android only!`);
}
}
export function test_FileAppend(done) {
const content = 'Hello World';
const hello_world = global.isIOS ? NSString.stringWithString(content).dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String(content).getBytes('UTF-8');
const file = fs.File.fromPath(fs.path.join(fs.knownFolders.temp().path, `${Date.now()}-app.txt`));
file
.appendText('Hello')
.then(() => file.appendText(' World'))
.then(() => {
TKUnit.assert(file.size === hello_world.length);
return file.readText();
})
.then((value) => {
TKUnit.assert(value === content);
return Promise.allSettled([file.remove()]);
})
.then(() => done())
.catch(done);
}
export function test_FileAppendText(done) {
const content = 'Hello World';
const file = fs.File.fromPath(fs.path.join(fs.knownFolders.temp().path, `${Date.now()}-app.txt`));
file
.appendText('Hello')
.then(() => file.appendText(' World'))
.then(() => file.readText())
.then((value) => {
TKUnit.assert(value === content);
return Promise.allSettled([file.remove()]);
})
.then(() => done())
.catch(done);
}

View File

@ -1,4 +1,4 @@
import { Page, EventData, Application, File, Folder, knownFolders, path, getFileAccess, Utils } from '@nativescript/core';
import { Page, EventData, Application, File, Folder, knownFolders, path, getFileAccess, Utils, Screen, Http, AndroidDirectory, ImageSource, alert } from '@nativescript/core';
let page: Page;
@ -217,3 +217,103 @@ function getFileNameFromContent(content: string) {
const file = getFileAccess().getFile(content);
return decodeURIComponent(file.name).split('/').pop().toLowerCase();
}
let lastDownload: File;
export function createFileInDownloads() {
if (!global.isAndroid) {
return;
}
const width = Screen.mainScreen.widthPixels;
const height = Screen.mainScreen.heightPixels;
const randomImageUrl = `https://picsum.photos/${width}/${height}.jpg?random=${Date.now()}`;
Http.getFile(randomImageUrl).then((result: File) => {
let file = File.android.createFile({
directory: AndroidDirectory.DOWNLOADS,
name: `${Date.now()}.jpg`,
mime: 'image/jpeg',
relativePath: `NativeScript`,
});
result
.copy(file.path)
.then((done) => {
lastDownload = file;
console.log('done: ' + done + '\n' + 'Original path: ' + result.path + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
alert(`File saved in ${AndroidDirectory.DOWNLOADS}/NativeScript`);
})
.catch((error) => {
console.error(error);
});
});
}
export function createFileInGallery() {
if (!global.isAndroid) {
return;
}
const width = Screen.mainScreen.widthPixels;
const height = Screen.mainScreen.heightPixels;
const randomImageUrl = `https://picsum.photos/${width}/${height}.jpg?random=${Date.now()}`;
Http.getFile(randomImageUrl).then((result: File) => {
let file = File.android.createFile({
directory: AndroidDirectory.PICTURES,
name: `${Date.now()}.jpg`,
mime: 'image/jpeg',
relativePath: `NativeScript`,
});
result
.copy(file.path)
.then((done) => {
console.log('done: ' + done + '\n' + 'Original path: ' + result + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
alert(`File saved in ${AndroidDirectory.PICTURES}/NativeScript`);
})
.catch((error) => {
console.error(error);
});
});
}
export function createFileInMusic() {
if (!global.isAndroid) {
return;
}
Http.getFile('https://github.com/rafaelreis-hotmart/Audio-Sample-files/raw/master/sample.mp3').then((result: File) => {
let file = File.android.createFile({
directory: AndroidDirectory.MUSIC,
name: `${Date.now()}.MP3`,
mime: 'audio/mp3',
relativePath: `NativeScript/MP3`,
});
result
.copy(file.path)
.then((done) => {
console.log('done: ' + done + '\n' + 'Original path: ' + result + '\n' + 'Copied to: ' + file.path + '\n' + 'Original size: ' + result.size + '\n' + 'Copy size: ' + file.size + '\n');
alert(`File saved in ${AndroidDirectory.MUSIC}/NativeScript/MP3`);
})
.catch((error) => {
console.error(error);
});
});
}
export function createAndAppendText() {
const file = File.fromPath(path.join(knownFolders.temp().path, `${Date.now()}-app.txt`));
file.appendTextSync('Hello');
file.appendTextSync(' World');
const data = file.readTextSync();
console.log('createAndAppend:', data === 'Hello World');
}
export function createAndAppendData() {
const file = File.fromPath(path.join(knownFolders.temp().path, `${Date.now()}-app.txt`));
const hello = global.isIOS ? NSString.stringWithString('Hello').dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String('Hello').getBytes('UTF-8');
const world = global.isIOS ? NSString.stringWithString(' World').dataUsingEncoding(NSUTF8StringEncoding) : new java.lang.String(' World').getBytes('UTF-8');
file.appendSync(hello);
file.appendSync(world);
const data = file.readTextSync();
console.log('createAndAppendData:', data === 'Hello World');
}

View File

@ -5,5 +5,10 @@
<Button text="Pick File" tap="pickFile" />
<Button text="Pick Multiple Files" tap="pickFiles" />
<Button text="Test Copy" tap="copyTest" />
<Button text="External File Creation" tap="createFileInDownloads" />
<Button text="Add file to gallery" tap="createFileInGallery" />
<Button text="Add file to music" tap="createFileInMusic" />
<Button text="Append Text" tap="createAndAppendText" />
<Button text="Append Data" tap="createAndAppendData" />
</StackLayout>
</Page>

View File

@ -375,6 +375,81 @@ export class FileSystemAccess implements IFileSystemAccess {
}
}
public appendBuffer = this.appendBufferSync.bind(this);
public appendBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
org.nativescript.widgets.Async.File.appendBuffer(
path,
FileSystemAccess.getBuffer(buffer),
new org.nativescript.widgets.Async.CompleteCallback({
onComplete: () => {
resolve();
},
onError: (err) => {
reject(new Error(err));
},
}),
null
);
} catch (ex) {
reject(ex);
}
});
}
public appendBufferSync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
try {
const javaFile = new java.io.File(path);
const stream = new java.io.FileOutputStream(javaFile);
const channel = stream.getChannel();
channel.write(FileSystemAccess.getBuffer(buffer));
stream.close();
} catch (exception) {
if (onError) {
onError(exception);
}
}
}
public append = this.appendSync.bind(this);
public appendAsync(path: string, bytes: androidNative.Array<number>): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
org.nativescript.widgets.Async.File.append(
path,
bytes,
new org.nativescript.widgets.Async.CompleteCallback({
onComplete: () => {
resolve();
},
onError: (err) => {
reject(new Error(err));
},
}),
null
);
} catch (ex) {
reject(ex);
}
});
}
public appendSync(path: string, bytes: androidNative.Array<number>, onError?: (error: any) => any) {
try {
const javaFile = new java.io.File(path);
const stream = new java.io.FileOutputStream(javaFile, true);
stream.write(bytes, 0, bytes.length);
stream.close();
} catch (exception) {
if (onError) {
onError(exception);
}
}
}
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
@ -538,6 +613,56 @@ export class FileSystemAccess implements IFileSystemAccess {
return s;
}
public appendText = this.appendTextSync.bind(this);
public appendTextAsync(path: string, content: string, encoding?: any): Promise<void> {
let actualEncoding = encoding;
if (!actualEncoding) {
actualEncoding = textModule.encoding.UTF_8;
}
return new Promise<void>((resolve, reject) => {
try {
org.nativescript.widgets.Async.File.appendText(
path,
content,
actualEncoding,
new org.nativescript.widgets.Async.CompleteCallback({
onComplete: () => {
resolve();
},
onError: (err) => {
reject(new Error(err));
},
}),
null
);
} catch (ex) {
reject(ex);
}
});
}
public appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
try {
const javaFile = new java.io.File(path);
const stream = new java.io.FileOutputStream(javaFile, true);
let actualEncoding = encoding;
if (!actualEncoding) {
actualEncoding = textModule.encoding.UTF_8;
}
const writer = new java.io.OutputStreamWriter(stream, actualEncoding);
writer.write(content);
writer.close();
} catch (exception) {
if (onError) {
onError(exception);
}
}
}
public writeText = this.writeTextSync.bind(this);
public writeTextAsync(path: string, content: string, encoding?: any): Promise<void> {
@ -879,6 +1004,124 @@ export class FileSystemAccess29 extends FileSystemAccess {
return super.getCurrentAppPath();
}
appendBuffer = this.appendBufferSync.bind(this);
appendBufferAsync(path: string, content: any): Promise<void> {
if (isContentUri(path)) {
return new Promise<void>((resolve, reject) => {
getOrSetHelper(path).appendBuffer(
applicationContext,
FileSystemAccess.getBuffer(content),
new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {
resolve();
},
onError(error) {
reject(error);
},
})
);
});
}
return super.appendAsync(path, content);
}
appendBufferSync(path: string, content: any, onError?: (error: any) => any) {
if (isContentUri(path)) {
let callback = null;
if (typeof onError === 'function') {
callback = new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {},
onError(error) {
onError(error);
},
});
}
getOrSetHelper(path).appendSync(applicationContext, FileSystemAccess.getBuffer(content), callback);
} else {
super.appendSync(path, content, onError);
}
}
append = this.appendSync.bind(this);
appendAsync(path: string, content: any): Promise<void> {
if (isContentUri(path)) {
return new Promise<void>((resolve, reject) => {
getOrSetHelper(path).append(
applicationContext,
content,
new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {
resolve();
},
onError(error) {
reject(error);
},
})
);
});
}
return super.appendAsync(path, content);
}
appendSync(path: string, content: any, onError?: (error: any) => any) {
if (isContentUri(path)) {
let callback = null;
if (typeof onError === 'function') {
callback = new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {},
onError(error) {
onError(error);
},
});
}
getOrSetHelper(path).appendSync(applicationContext, content, callback);
} else {
super.appendSync(path, content, onError);
}
}
appendText = this.appendTextSync.bind(this);
appendTextAsync(path: string, content: string, encoding?: any): Promise<void> {
if (isContentUri(path)) {
return new Promise<void>((resolve, reject) => {
getOrSetHelper(path).appendText(
applicationContext,
content,
encoding ?? null,
new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {
resolve();
},
onError(error) {
reject(error);
},
})
);
});
}
return super.appendTextAsync(path, content, encoding);
}
appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
if (isContentUri(path)) {
let callback = null;
if (typeof onError === 'function') {
callback = new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {},
onError(error) {
onError(error);
},
});
}
getOrSetHelper(path).appendTextSync(applicationContext, content, encoding ?? null, callback);
} else {
super.appendTextSync(path, content, onError);
}
}
public readText = this.readTextSync.bind(this);
readTextAsync(path: string, encoding?: any): Promise<string> {

View File

@ -2,6 +2,55 @@
* An utility class used to provide methods to access and work with the file system.
*/
export interface IFileSystemAccess {
/**
* Appends binary to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
* @param onError (optional) A callback function to use if any error occurs.
*/
append(path: string, content: any, onError?: (error: any) => any);
/**
* Appends binary to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
*/
appendAsync(path: string, content: any): Promise<void>;
/**
* Appends a binary to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
* @param onError (optional) A callback function to use if any error occurs.
*/
appendSync(path: string, content: any, onError?: (error: any) => any);
/**
* Appends text to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
* @param onError (optional) A callback function to use if any error occurs.
* @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
*/
appendText(path: string, content: string, onError?: (error: any) => any, encoding?: any);
/**
* Appends text to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
* @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
*/
appendTextAsync(path: string, content: string, encoding?: any): Promise<void>;
/**
* Appends text to a file with a given path.
* @param path The path to the source file.
* @param content The content which will be written to the file.
* @param onError (optional) A callback function to use if any error occurs.
* @param encoding (optional) If set writes the text with the specified encoding (default UTF-8).
*/
appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
/**
* Copies a file to a given path.
* @param src The path to the source file.
@ -282,6 +331,24 @@ export interface IFileSystemAccess {
}
export class FileSystemAccess implements IFileSystemAccess {
appendBuffer(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
appendBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void>;
appendBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
append(path: string, content: any, onError?: (error: any) => any);
appendAsync(path: string, content: any): Promise<void>;
appendSync(path: string, content: any, onError?: (error: any) => any);
appendText(path: string, content: string, onError?: (error: any) => any, encoding?: any);
appendTextAsync(path: string, content: string, encoding?: any): Promise<void>;
appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
copy(src: string, dest: string, onError?: (error: any) => any): boolean;
copySync(src: string, dest: string, onError?: (error: any) => any): boolean;

View File

@ -445,6 +445,115 @@ export class FileSystemAccess {
}
}
public appendBuffer = this.appendBufferSync.bind(this);
public appendBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
(handle as any).appendDataCompletion(FileSystemAccess.getBuffer(content), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
handle.closeFile();
});
} catch (ex) {
reject(new Error("Failed to write file at path '" + path + "': " + ex));
}
});
}
public appendBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
try {
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
handle.seekToEndOfFile();
handle.writeData(FileSystemAccess.getBuffer(content));
handle.closeFile();
} catch (ex) {
if (onError) {
onError(new Error("Failed to write to file '" + path + "': " + ex));
}
}
}
public append = this.appendSync.bind(this);
public appendAsync(path: string, content: NSData): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
(handle as any).appendDataCompletion(content, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
handle.closeFile();
});
} catch (ex) {
reject(new Error("Failed to write file at path '" + path + "': " + ex));
}
});
}
public appendSync(path: string, content: NSData, onError?: (error: any) => any) {
try {
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
handle.seekToEndOfFile();
handle.writeData(content);
handle.closeFile();
} catch (ex) {
if (onError) {
onError(new Error("Failed to write to file '" + path + "': " + ex));
}
}
}
public appendText = this.appendTextSync.bind(this);
public appendTextAsync(path: string, content: string, encoding?: any): Promise<void> {
const nsString = NSString.stringWithString(content);
const actualEncoding = encoding || textEncoding.UTF_8;
return new Promise<void>((resolve, reject) => {
try {
const data = nsString.dataUsingEncoding(actualEncoding);
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
(handle as any).appendDataCompletion(data, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
handle.closeFile();
});
} catch (ex) {
reject(new Error("Failed to append file at path '" + path + "': " + ex));
}
});
}
public appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
const nsString = NSString.stringWithString(content);
const actualEncoding = encoding || textEncoding.UTF_8;
// TODO: verify the useAuxiliaryFile parameter should be false
try {
const data = nsString.dataUsingEncoding(actualEncoding);
const handle = NSFileHandle.fileHandleForWritingAtPath(path);
handle.seekToEndOfFile();
handle.writeData(data);
handle.closeFile();
} catch (ex) {
if (onError) {
onError(new Error("Failed to append to file '" + path + "': " + ex));
}
}
}
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {

View File

@ -1,5 +1,20 @@
import { FileSystemAccess } from './file-system-access';
export enum AndroidDirectory {
ALARMS,
AUDIOBOOKS,
DCIM,
DOCUMENTS,
DOWNLOADS,
MOVIES,
MUSIC,
NOTIFICATIONS,
PICTURES,
PODCASTS,
RINGTONES,
SCREENSHOTS,
}
/**
* Returns FileSystemAccess, a shared singleton utility class to provide methods to access and work with the file system. This is used under the hood of all the file system apis in @nativescript/core and provided as a lower level convenience if needed.
* @returns FileSystemAccess
@ -55,10 +70,27 @@ export class FileSystemEntity {
renameSync(newName: string, onError?: (error: any) => any): void;
}
/**
* Contains Android-specific the file system helpers.
*/
class Android {
createFile(options: { relativePath?: string; name: string; mime: string; directory: AndroidDirectory }): File;
}
/**
* Contains iOS-specific the file system helpers.
*/
class iOS {}
/**
* Represents a File entity on the file system.
*/
export class File extends FileSystemEntity {
static readonly android: Android;
static readonly ios: iOS;
/**
* Checks whether a File with the specified path already exists.
* @param path The path to check for.
@ -80,6 +112,34 @@ export class File extends FileSystemEntity {
*/
isLocked: boolean;
/**
* Appends the provided string to the file, using the specified encoding (defaults to UTF-8).
* @param content The content to be saved to the file.
* @param encoding An optional value specifying the preferred encoding (defaults to UTF-8).
*/
appendText(content: string, encoding?: string): Promise<any>;
/**
* Appends the provided string to the file synchronously, using the specified encoding (defaults to UTF-8).
* @param content The content to be saved to the file.
* @param onError An optional function to be called if some IO-error occurs.
* @param encoding An optional value specifying the preferred encoding (defaults to UTF-8).
*/
appendTextSync(content: string, onError?: (error: any) => any, encoding?: string): void;
/**
* Appends the provided binary content to the file.
* @param content The binary content to be saved to the file.
*/
append(content: any): Promise<void>;
/**
* Appends the provided binary content to the file synchronously.
* @param content The binary content to be saved to the file.
* @param onError An optional function to be called if some IO-error occurs.
*/
appendSync(content: any, onError?: (error: any) => any): void;
/**
* Copies a file to a given path.
* @param dest The path to the destination file.

View File

@ -190,7 +190,136 @@ function getApplicationContext() {
return applicationContext;
}
export enum AndroidDirectory {
ALARMS = 'alarms',
AUDIOBOOKS = 'audiobooks',
DCIM = 'dcim',
DOCUMENTS = 'documents',
DOWNLOADS = 'downloads',
MOVIES = 'movies',
MUSIC = 'music',
NOTIFICATIONS = 'notifications',
PICTURES = 'pictures',
PODCASTS = 'podcasts',
RINGTONES = 'ringtones',
SCREENSHOTS = 'screenshots',
}
function getAndroidDirectory(value: AndroidDirectory): { path: string; column: android.net.Uri } | null {
switch (value) {
case AndroidDirectory.ALARMS:
return {
path: android.os.Environment.DIRECTORY_ALARMS,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.AUDIOBOOKS:
return {
path: android.os.Environment.DIRECTORY_AUDIOBOOKS,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.DCIM:
return {
path: android.os.Environment.DIRECTORY_DCIM,
column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.DOCUMENTS:
return {
path: android.os.Environment.DIRECTORY_DOCUMENTS,
column: android.provider.MediaStore.Files.getContentUri('external'),
};
case AndroidDirectory.DOWNLOADS:
return {
path: android.os.Environment.DIRECTORY_DOWNLOADS,
column: android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.MOVIES:
return {
path: android.os.Environment.DIRECTORY_MOVIES,
column: android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.MUSIC:
return {
path: android.os.Environment.DIRECTORY_MUSIC,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.NOTIFICATIONS:
return {
path: android.os.Environment.DIRECTORY_NOTIFICATIONS,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.PICTURES:
return {
path: android.os.Environment.DIRECTORY_PICTURES,
column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.PODCASTS:
return {
path: android.os.Environment.DIRECTORY_PODCASTS,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.RINGTONES:
return {
path: android.os.Environment.DIRECTORY_RINGTONES,
column: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
};
case AndroidDirectory.SCREENSHOTS:
return {
path: android.os.Environment.DIRECTORY_SCREENSHOTS,
column: android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
};
default:
return null;
}
}
class Android {
createFile(options: { relativePath?: string; name: string; mime: string; directory: AndroidDirectory }): File {
if (!global.isAndroid) {
throw new Error(`createFile is available on Android only!`);
}
const context = getApplicationContext() as android.content.Context;
const meta = new android.content.ContentValues();
meta.put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, options.name);
meta.put(android.provider.MediaStore.MediaColumns.MIME_TYPE, options.mime);
//meta.put(android.provider.MediaStore.MediaColumns.DATE_ADDED, java.lang.System.currentTimeMillis() as any);
const externalDirectory = getAndroidDirectory(options.directory);
if (SDK_VERSION >= 29) {
const relativePath = options?.relativePath ? `/${options.relativePath}` : '';
meta.put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, `${externalDirectory.path}${relativePath}`);
// todo
// meta.put(android.provider.MediaStore.MediaColumns.IS_PENDING, java.lang.Integer.valueOf(1));
} else {
const relativePath = options?.relativePath ? `${options.relativePath}/` : '';
const directory = android.os.Environment.getExternalStoragePublicDirectory(externalDirectory.path);
const file = new java.io.File(directory, `${relativePath}${options.name}`);
meta.put(android.provider.MediaStore.MediaColumns.DATA, file.getAbsolutePath());
}
const uri = context.getContentResolver().insert(externalDirectory.column, meta);
return File.fromPath(uri.toString());
}
}
const ad = new Android();
class iOS {}
const ios = new iOS();
export class File extends FileSystemEntity {
public static get ios() {
return ios;
}
public static get android() {
return ad;
}
public static fromPath(path: string, copy: boolean = false) {
const onError = function (error) {
throw error;
@ -239,6 +368,100 @@ export class File extends FileSystemEntity {
return getFileAccess().getFileSize(this.path);
}
public append(content: any): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
this._checkAccess();
} catch (ex) {
reject(ex);
return;
}
this._locked = true;
getFileAccess()
.appendAsync(this.path, content)
.then(
() => {
resolve();
this._locked = false;
},
(error) => {
reject(error);
this._locked = false;
}
);
});
}
public appendSync(content: any, onError?: (error: any) => any): void {
this._checkAccess();
try {
this._locked = true;
const that = this;
const localError = function (error) {
that._locked = false;
if (onError) {
onError(error);
}
};
getFileAccess().appendSync(this.path, content, localError);
} finally {
this._locked = false;
}
}
public appendText(content: string, encoding?: string): Promise<any> {
return new Promise((resolve, reject) => {
try {
this._checkAccess();
} catch (ex) {
reject(ex);
return;
}
this._locked = true;
getFileAccess()
.appendTextAsync(this.path, content, encoding)
.then(
() => {
resolve(true);
this._locked = false;
},
(error) => {
reject(error);
this._locked = false;
}
);
});
}
public appendTextSync(content: string, onError?: (error: any) => any, encoding?: string): void {
this._checkAccess();
try {
this._locked = true;
const that = this;
const localError = function (error) {
that._locked = false;
if (onError) {
onError(error);
}
};
getFileAccess().appendTextSync(this.path, content, localError, encoding);
} finally {
this._locked = false;
}
}
public copy(dest: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {

View File

@ -85,7 +85,7 @@ export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './dat
export type { PropertyChangeData, EventData } from './data/observable';
export { VirtualArray } from './data/virtual-array';
export type { ItemsLoading } from './data/virtual-array';
export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess } from './file-system';
export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess, AndroidDirectory } from './file-system';
export type { HttpRequestOptions, HttpResponse, Headers, HttpResponseEncoding, HttpContent } from './http';
import { getFile, getImage, getJSON, getString as httpGetString } from './http';
export declare const Http: {

View File

@ -105,7 +105,7 @@ export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './dat
export type { PropertyChangeData, EventData } from './data/observable';
export { VirtualArray } from './data/virtual-array';
export type { ItemsLoading } from './data/virtual-array';
export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess } from './file-system';
export { File, FileSystemEntity, Folder, knownFolders, path, getFileAccess, AndroidDirectory } from './file-system';
// Export all interfaces from "http" module
export type { HttpRequestOptions, HttpResponse, Headers, HttpResponseEncoding, HttpContent } from './http';

View File

@ -4,6 +4,23 @@
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>DebugSymbolsPath</key>
<string>dSYMs</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-maccatalyst</string>
<key>LibraryPath</key>
<string>TNSWidgets.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>maccatalyst</string>
</dict>
<dict>
<key>DebugSymbolsPath</key>
<string>dSYMs</string>
@ -35,23 +52,6 @@
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>DebugSymbolsPath</key>
<string>dSYMs</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-maccatalyst</string>
<key>LibraryPath</key>
<string>TNSWidgets.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>maccatalyst</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>

View File

@ -0,0 +1,22 @@
//
// NSFileHandle+Async.h
// TNSWidgets
//
// Created by Osei Fortune on 03/05/2023.
// Copyright © 2023 Telerik A D. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSFileHandle (Async)
- (void)appendData:(nonnull NSData*) data
completion:(void (^) (NSError*))callback;
+ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
@end
NS_ASSUME_NONNULL_END

View File

@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
#import "NSFileHandle+Async.h"

View File

@ -0,0 +1,22 @@
//
// NSFileHandle+Async.h
// TNSWidgets
//
// Created by Osei Fortune on 03/05/2023.
// Copyright © 2023 Telerik A D. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSFileHandle (Async)
- (void)appendData:(nonnull NSData*) data
completion:(void (^) (NSError*))callback;
+ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
@end
NS_ASSUME_NONNULL_END

View File

@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
#import "NSFileHandle+Async.h"

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>21G72</string>
<string>22D68</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
@ -29,7 +29,7 @@
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>13F100</string>
<string>14A309</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
@ -39,9 +39,9 @@
<key>DTSDKName</key>
<string>macosx12.3</string>
<key>DTXcode</key>
<string>1341</string>
<string>1400</string>
<key>DTXcodeBuild</key>
<string>13F100</string>
<string>14A309</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>UIDeviceFamily</key>

View File

@ -0,0 +1,22 @@
//
// NSFileHandle+Async.h
// TNSWidgets
//
// Created by Osei Fortune on 03/05/2023.
// Copyright © 2023 Telerik A D. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSFileHandle (Async)
- (void)appendData:(nonnull NSData*) data
completion:(void (^) (NSError*))callback;
+ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
@end
NS_ASSUME_NONNULL_END

View File

@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
#import "NSFileHandle+Async.h"

View File

@ -8,6 +8,10 @@
<data>
9BsuK8QsA57YnHHgpWIgaHygSEk=
</data>
<key>Headers/NSFileHandle+Async.h</key>
<data>
iQtzsDHw/VTnFG27yW+QvGCeXGw=
</data>
<key>Headers/NSString+Async.h</key>
<data>
o8366y9zYMOVyObC3vtKKy/8jxA=
@ -22,7 +26,7 @@
</data>
<key>Headers/TNSWidgets.h</key>
<data>
gUvu5bjZg5Aie5iJ1krxFmDrHwk=
ZFbCov7mFiXa4ZA/gAxqYHzRX7Q=
</data>
<key>Headers/UIImage+TNSBlocks.h</key>
<data>
@ -34,7 +38,7 @@
</data>
<key>Info.plist</key>
<data>
RCfacSfbjEiH19hn34SSJzGdiFo=
myeIggkwNuTIqt7xlMgf8VGsKZ8=
</data>
<key>Modules/module.modulemap</key>
<data>
@ -58,6 +62,13 @@
JdjuVUBed00Ged4cSDzYLXONUlESu+dae9KN0PYJ/nM=
</data>
</dict>
<key>Headers/NSFileHandle+Async.h</key>
<dict>
<key>hash2</key>
<data>
1IqS81dD1dbKccHZ0lYPMMF1zBPdp3InM+rdOFjBo+k=
</data>
</dict>
<key>Headers/NSString+Async.h</key>
<dict>
<key>hash2</key>
@ -83,7 +94,7 @@
<dict>
<key>hash2</key>
<data>
V99t2zLwRPOs90tbGiQbhbdAFJlW7mp7X2R5337ewUA=
6L3xStgKRHkPvDZAHAUF+EMI3Skf5jvnoA4ED/WbB1w=
</data>
</dict>
<key>Headers/UIImage+TNSBlocks.h</key>

View File

@ -39,6 +39,9 @@
export function writeText(path: string, content: string, encoding: string, callback: CompleteCallback, context: any);
export function write(path: string, content: androidNative.Array<number>, callback: CompleteCallback, context: any);
export function writeBuffer(param0: string, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.Async.CompleteCallback, param3: any): void;
export function append(path: string, content: androidNative.Array<number>, callback: CompleteCallback, context: any);
export function appendBuffer(param0: string, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.Async.CompleteCallback, param3: any): void;
export function appendText(path: string, content: string, encoding: string, callback: CompleteCallback, context: any);
}
export module Http {
@ -646,7 +649,13 @@ declare module org {
export module widgets {
export class FileHelper {
public static class: java.lang.Class<org.nativescript.widgets.FileHelper>;
public appendTextSync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
public appendText(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
public readText(param0: globalAndroid.content.Context, param1: string, param2: org.nativescript.widgets.FileHelper.Callback): void;
public appendSync(param0: globalAndroid.content.Context, param1: androidNative.Array<number>, param2: org.nativescript.widgets.FileHelper.Callback): void;
public append(param0: globalAndroid.content.Context, param1: androidNative.Array<number>, param2: org.nativescript.widgets.FileHelper.Callback): void;
public appendBufferSync(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void;
public appendBuffer(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void;
public writeBufferSync(param0: globalAndroid.content.Context, param1: java.nio.ByteBuffer, param2: org.nativescript.widgets.FileHelper.Callback): void;
public writeTextSync(param0: globalAndroid.content.Context, param1: string, param2: string, param3: org.nativescript.widgets.FileHelper.Callback): void;
public copyToFileSync(param0: globalAndroid.content.Context, param1: java.io.File, param2: org.nativescript.widgets.FileHelper.Callback): boolean;

View File

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.nativescript.widgets">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<application
android:allowBackup="true">

View File

@ -1,11 +1,13 @@
package org.nativescript.widgets;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
@ -599,19 +601,87 @@ public class Async {
public static class File {
static void updateValue(Context context, Uri uri) {
ContentValues values = new ContentValues();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.getContentResolver().update(uri, values, null);
} else {
context.getContentResolver().update(uri, values, null, null);
}
}
public static void append(final String path, final byte[] content, final CompleteCallback callback, final Object context) {
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
threadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
final AppendTask task = new AppendTask(callback, context);
final boolean result = task.doInBackground(path, content);
mHandler.post(new Runnable() {
@Override
public void run() {
task.onPostExecute(result);
}
});
}
});
}
public static void appendBuffer(final String path, final ByteBuffer content, final CompleteCallback callback, final Object context) {
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
threadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
final AppendBufferTask task = new AppendBufferTask(callback, context);
final boolean result = task.doInBackground(path, content);
mHandler.post(new Runnable() {
@Override
public void run() {
task.onPostExecute(result);
}
});
}
});
}
public static void appendText(final String path, final String content, final String encoding, final CompleteCallback callback, final Object context) {
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
threadPoolExecutor().execute(new Runnable() {
@Override
public void run() {
final AppendTextTask task = new AppendTextTask(callback, context);
final boolean result = task.doInBackground(path, content, encoding);
mHandler.post(new Runnable() {
@Override
public void run() {
task.onPostExecute(result);
}
});
}
});
}
public static boolean copySync(final String src, final String dest, final Context context) throws Exception {
InputStream is;
OutputStream os;
boolean requiresUpdate = false;
if(src.startsWith("content://")){
is = context.getContentResolver().openInputStream(Uri.parse(src));
}else is = new FileInputStream(new java.io.File(src));
if(dest.startsWith("content://")){
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
requiresUpdate = true;
}else os = new FileOutputStream(new java.io.File(dest));
return copySync(is, os, context);
boolean ret = copySync(is, os, context);
if(ret && requiresUpdate){
updateValue(context, Uri.parse(dest));
}
return ret;
}
public static boolean copySync(final InputStream src, final OutputStream dest, final Object context) throws Exception {
@ -630,15 +700,33 @@ public class Async {
InputStream is;
OutputStream os;
boolean requiresUpdate = false;
if(src.startsWith("content://")){
is = context.getContentResolver().openInputStream(Uri.parse(src));
}else is = new FileInputStream(new java.io.File(src));
if(dest.startsWith("content://")){
requiresUpdate = true;
os = context.getContentResolver().openOutputStream(Uri.parse(dest));
}else os = new FileOutputStream(new java.io.File(dest));
copy(is, os, callback, context);
boolean finalRequiresUpdate = requiresUpdate;
copy(is, os, new CompleteCallback() {
@Override
public void onComplete(Object result, Object tag) {
if(finalRequiresUpdate){
updateValue(context, Uri.parse(dest));
}
callback.onComplete(result, tag);
}
@Override
public void onError(String error, Object tag) {
callback.onError(error, tag);
}
}, context);
}catch (Exception exception){
callback.onError(exception.getMessage(), context);
}
@ -666,11 +754,12 @@ public class Async {
return written;
}
public static void copy(final InputStream src, final OutputStream dest, final CompleteCallback callback, final Object context) {
final android.os.Handler mHandler = new android.os.Handler(Looper.myLooper());
threadPoolExecutor().execute((Runnable) () -> {
boolean done = false;
Exception error = null;
try (InputStream is = src; OutputStream os = dest){
ReadableByteChannel isc = java.nio.channels.Channels.newChannel(is);
WritableByteChannel osc = java.nio.channels.Channels.newChannel(os);
@ -679,10 +768,19 @@ public class Async {
int written = fastChannelCopy(isc, osc);
mHandler.post(() -> callback.onComplete(size == written, context));
done = size == written;
} catch (Exception e) {
mHandler.post(() -> callback.onError(e.getMessage(), context));
error = e;
}finally {
if (error != null){
Exception finalError = error;
mHandler.post(() -> callback.onError(finalError.getMessage(), context));
}else {
boolean finalDone = done;
mHandler.post(() -> callback.onComplete(finalDone, context));
}
}
});
}
@ -1086,5 +1184,146 @@ public class Async {
}
}
static class AppendTask {
private final CompleteCallback callback;
private final Object context;
public AppendTask(CompleteCallback callback, Object context) {
this.callback = callback;
this.context = context;
}
protected boolean doInBackground(Object... params) {
java.io.File javaFile = new java.io.File((String) params[0]);
FileOutputStream stream = null;
byte[] content = (byte[]) params[1];
try {
stream = new FileOutputStream(javaFile, true);
stream.write(content, 0, content.length);
return true;
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to append file, FileNotFoundException: " + e.getMessage());
return false;
} catch (IOException e) {
Log.e(TAG, "Failed to write file, IOException: " + e.getMessage());
return false;
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage());
}
}
}
}
protected void onPostExecute(final boolean result) {
if (result) {
this.callback.onComplete(null, this.context);
} else {
this.callback.onError("AppendTask returns no result.", this.context);
}
}
}
static class AppendBufferTask {
private final CompleteCallback callback;
private final Object context;
public AppendBufferTask(CompleteCallback callback, Object context) {
this.callback = callback;
this.context = context;
}
protected boolean doInBackground(Object... params) {
java.io.File javaFile = new java.io.File((String) params[0]);
FileOutputStream stream = null;
ByteBuffer content = (ByteBuffer) params[1];
try {
stream = new FileOutputStream(javaFile, true);
FileChannel channel = stream.getChannel();
content.rewind();
channel.write(content);
content.rewind();
return true;
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to append to file, FileNotFoundException: " + e.getMessage());
return false;
} catch (IOException e) {
Log.e(TAG, "Failed to append file, IOException: " + e.getMessage());
return false;
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage());
}
}
}
}
protected void onPostExecute(final boolean result) {
if (result) {
this.callback.onComplete(null, this.context);
} else {
this.callback.onError("AppendTask returns no result.", this.context);
}
}
}
static class AppendTextTask {
private final CompleteCallback callback;
private final Object context;
public AppendTextTask(CompleteCallback callback, Object context) {
this.callback = callback;
this.context = context;
}
protected boolean doInBackground(String... params) {
java.io.File javaFile = new java.io.File(params[0]);
FileOutputStream stream = null;
try {
stream = new FileOutputStream(javaFile, true);
OutputStreamWriter writer = new OutputStreamWriter(stream, params[2]);
writer.write(params[1]);
writer.close();
return true;
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to append file, FileNotFoundException: " + e.getMessage());
return false;
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to append file, UnsupportedEncodingException: " + e.getMessage());
return false;
} catch (IOException e) {
Log.e(TAG, "Failed to append file, IOException: " + e.getMessage());
return false;
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage());
}
}
}
}
protected void onPostExecute(final boolean result) {
if (result) {
this.callback.onComplete(null, this.context);
} else {
this.callback.onError("AppendTextTask returns no result.", this.context);
}
}
}
}
}

View File

@ -19,6 +19,8 @@
D004031022F781A50089EAD8 /* NSString+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = D004030E22F781A50089EAD8 /* NSString+Async.m */; };
D004031322FA27D60089EAD8 /* NSData+Async.h in Headers */ = {isa = PBXBuildFile; fileRef = D004031122FA27D60089EAD8 /* NSData+Async.h */; settings = {ATTRIBUTES = (Public, ); }; };
D004031422FA27D60089EAD8 /* NSData+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = D004031222FA27D60089EAD8 /* NSData+Async.m */; };
F11DE2162A02F39A00B70DC5 /* NSFileHandle+Async.m in Sources */ = {isa = PBXBuildFile; fileRef = F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */; };
F11DE2182A02F3B500B70DC5 /* NSFileHandle+Async.h in Headers */ = {isa = PBXBuildFile; fileRef = F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */; settings = {ATTRIBUTES = (Public, ); }; };
F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */ = {isa = PBXBuildFile; fileRef = F915D3531EC9EF5E00071914 /* TNSProcess.m */; };
F915D3561EC9EF5E00071914 /* TNSProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = F915D3541EC9EF5E00071914 /* TNSProcess.h */; settings = {ATTRIBUTES = (Public, ); }; };
F98F5CB31CD0EFEA00978308 /* TNSWidgets.h in Headers */ = {isa = PBXBuildFile; fileRef = F98F5CB21CD0EFEA00978308 /* TNSWidgets.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -51,6 +53,8 @@
D004030E22F781A50089EAD8 /* NSString+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+Async.m"; sourceTree = "<group>"; };
D004031122FA27D60089EAD8 /* NSData+Async.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Async.h"; sourceTree = "<group>"; };
D004031222FA27D60089EAD8 /* NSData+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Async.m"; sourceTree = "<group>"; };
F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFileHandle+Async.m"; sourceTree = "<group>"; };
F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSFileHandle+Async.h"; sourceTree = "<group>"; };
F915D3531EC9EF5E00071914 /* TNSProcess.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TNSProcess.m; path = ../TNSProcess.m; sourceTree = "<group>"; };
F915D3541EC9EF5E00071914 /* TNSProcess.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TNSProcess.h; path = ../TNSProcess.h; sourceTree = "<group>"; };
F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TNSWidgets.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -121,6 +125,8 @@
B8E76F5D212C3134009CFCE2 /* UIView+PassThroughParent.m */,
D004031122FA27D60089EAD8 /* NSData+Async.h */,
D004031222FA27D60089EAD8 /* NSData+Async.m */,
F11DE2172A02F3B500B70DC5 /* NSFileHandle+Async.h */,
F11DE2152A02F39A00B70DC5 /* NSFileHandle+Async.m */,
);
path = TNSWidgets;
sourceTree = "<group>";
@ -147,6 +153,7 @@
D004031322FA27D60089EAD8 /* NSData+Async.h in Headers */,
F98F5CCB1CD0F09E00978308 /* UIImage+TNSBlocks.h in Headers */,
B8E76F52212C2DA2009CFCE2 /* NSObject+Swizzling.h in Headers */,
F11DE2182A02F3B500B70DC5 /* NSFileHandle+Async.h in Headers */,
B8E76F5A212C2F4E009CFCE2 /* UIView+PropertyBag.h in Headers */,
D004030F22F781A50089EAD8 /* NSString+Async.h in Headers */,
8B7321CF1D097ECD00884AC6 /* TNSLabel.h in Headers */,
@ -252,6 +259,7 @@
files = (
8B7321D01D097ECD00884AC6 /* TNSLabel.m in Sources */,
F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */,
F11DE2162A02F39A00B70DC5 /* NSFileHandle+Async.m in Sources */,
F98F5CCC1CD0F09E00978308 /* UIImage+TNSBlocks.m in Sources */,
B8E76F53212C2DA2009CFCE2 /* NSObject+Swizzling.m in Sources */,
D004031022F781A50089EAD8 /* NSString+Async.m in Sources */,

View File

@ -0,0 +1,22 @@
//
// NSFileHandle+Async.h
// TNSWidgets
//
// Created by Osei Fortune on 03/05/2023.
// Copyright © 2023 Telerik A D. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSFileHandle (Async)
- (void)appendData:(nonnull NSData*) data
completion:(void (^) (NSError*))callback;
+ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*,NSError*))callback;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,82 @@
//
// NSFileHandle+Async.m
// TNSWidgets
//
// Created by Osei Fortune on 03/05/2023.
// Copyright © 2023 Telerik A D. All rights reserved.
//
#import "NSFileHandle+Async.h"
@implementation NSFileHandle(Async)
- (void)appendData:(NSData *)data completion:(void (^)(NSError*))callback {
dispatch_queue_t asyncQueue = dispatch_queue_create("org.nativescript.TNSWidgets.fileHandle", NULL);
dispatch_async(asyncQueue, ^(void) {
NSError *error = nil;
if (@available(iOS 13.0, *)) {
[self seekToEndReturningOffset:nil error:&error];
[self writeData:data error:&error];
} else {
@try {
[self seekToEndOfFile];
[self writeData:data];
} @catch (NSException *exception) {
NSMutableDictionary * info = [NSMutableDictionary dictionary];
[info setValue:exception.name forKey:@"ExceptionName"];
[info setValue:exception.reason forKey:@"ExceptionReason"];
[info setValue:exception.callStackReturnAddresses forKey:@"ExceptionCallStackReturnAddresses"];
[info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
[info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
error = [[NSError alloc] initWithDomain:@"" code: 1 userInfo:info];
}
}
dispatch_async(dispatch_get_main_queue(), ^(void) {
callback(error);
});
});
}
+ (void)fileHandleWith:(NSString *)path data:(NSData *)data completion:(void (^)(NSFileHandle*, NSError*))callback {
dispatch_queue_t asyncQueue = dispatch_queue_create("org.nativescript.TNSWidgets.fileHandle", NULL);
dispatch_async(asyncQueue, ^(void) {
NSFileHandle* handle = [NSFileHandle fileHandleForWritingAtPath: path];
NSError *error = nil;
if (@available(iOS 13.0, *)) {
[handle seekToEndReturningOffset:nil error:&error];
[handle writeData:data error:&error];
} else {
@try {
[handle seekToEndOfFile];
[handle writeData:data];
} @catch (NSException *exception) {
NSMutableDictionary * info = [NSMutableDictionary dictionary];
[info setValue:exception.name forKey:@"ExceptionName"];
[info setValue:exception.reason forKey:@"ExceptionReason"];
[info setValue:exception.callStackReturnAddresses forKey:@"ExceptionCallStackReturnAddresses"];
[info setValue:exception.callStackSymbols forKey:@"ExceptionCallStackSymbols"];
[info setValue:exception.userInfo forKey:@"ExceptionUserInfo"];
error = [[NSError alloc] initWithDomain:@"" code: 1 userInfo:info];
}
}
dispatch_async(dispatch_get_main_queue(), ^(void) {
callback(handle,error);
});
});
}
@end

View File

@ -22,3 +22,4 @@ FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[];
#import "TNSProcess.h"
#import "NSString+Async.h"
#import "NSData+Async.h"
#import "NSFileHandle+Async.h"