From 1d1fbfe4464a9cca9871a44e8e57c6cecf32b367 Mon Sep 17 00:00:00 2001 From: Jason Cassidy <47318351+jcassidyav@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:55:20 +0100 Subject: [PATCH 1/4] Initial in the middle of refactor --- packages/imagepicker/common.ts | 20 ++++++++++ packages/imagepicker/index.android.ts | 37 +++++++++--------- packages/imagepicker/index.d.ts | 2 +- packages/imagepicker/index.ios.ts | 38 +++++++++---------- .../Android/src/main/AndroidManifest.xml | 1 - tools/demo/imagepicker/index.ts | 3 +- 6 files changed, 60 insertions(+), 41 deletions(-) diff --git a/packages/imagepicker/common.ts b/packages/imagepicker/common.ts index a091f949..59513e36 100644 --- a/packages/imagepicker/common.ts +++ b/packages/imagepicker/common.ts @@ -1,4 +1,5 @@ import { ImageAsset } from '@nativescript/core'; +import { MultiResult, Result } from '@nativescript-community/perms'; export enum ImagePickerMediaType { Any = 0, @@ -116,3 +117,22 @@ export interface Options { read_external_storage?: string; }; } + +export interface ImagePickerApi { + /** + * Call this before 'present' to request any additional permissions that may be necessary. + * In case of failed authorization consider notifying the user for degraded functionality. + */ + authorize(): Promise; + + /** + * Present the image picker UI. + * The result will be an array of SelectedAsset instances provided when the promise is fulfilled. + */ + present(): Promise; +} + +export interface AuthorizationResult { + authorised: boolean; + details: MultiResult | Result; +} diff --git a/packages/imagepicker/index.android.ts b/packages/imagepicker/index.android.ts index 6268df41..7a2386e8 100644 --- a/packages/imagepicker/index.android.ts +++ b/packages/imagepicker/index.android.ts @@ -1,12 +1,12 @@ import { ImageAsset, Application, AndroidApplication, Utils, File, knownFolders } from '@nativescript/core'; import * as permissions from '@nativescript-community/perms'; -import { ImagePickerMediaType, Options } from './common'; +import { ImagePickerMediaType, Options, ImagePickerApi } from './common'; export * from './common'; let copyToAppFolder; let renameFileTo; -let fileMap = {}; -let videoFiles = { + +const videoFiles = { mp4: true, mov: true, avi: true, @@ -22,8 +22,8 @@ let videoFiles = { }; class UriHelper { public static _calculateFileUri(uri: android.net.Uri) { - let DocumentsContract = (android.provider).DocumentsContract; - let isKitKat = android.os.Build.VERSION.SDK_INT >= 19; // android.os.Build.VERSION_CODES.KITKAT + const DocumentsContract = (android.provider).DocumentsContract; + const isKitKat = android.os.Build.VERSION.SDK_INT >= 19; // android.os.Build.VERSION_CODES.KITKAT if (isKitKat && DocumentsContract.isDocumentUri(Utils.android.getApplicationContext(), uri)) { let docId, id, type; @@ -56,7 +56,7 @@ class UriHelper { // MediaProvider else if (UriHelper.isMediaDocument(uri)) { docId = DocumentsContract.getDocumentId(uri); - let split = docId.split(':'); + const split = docId.split(':'); type = split[0]; id = split[1]; @@ -68,8 +68,8 @@ class UriHelper { contentUri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } - let selection = '_id=?'; - let selectionArgs = [id]; + const selection = '_id=?'; + const selectionArgs = [id]; return UriHelper.getDataColumn(contentUri, selection, selectionArgs, false); } @@ -89,13 +89,13 @@ class UriHelper { private static getDataColumn(uri: android.net.Uri, selection, selectionArgs, isDownload: boolean) { let cursor = null; - let filePath; + let filePath: string; if (isDownload) { - let columns = ['_display_name']; + const columns = ['_display_name']; try { cursor = this.getContentResolver().query(uri, columns, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { - let column_index = cursor.getColumnIndexOrThrow(columns[0]); + const column_index = cursor.getColumnIndexOrThrow(columns[0]); filePath = cursor.getString(column_index); if (filePath) { const dl = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS); @@ -111,13 +111,13 @@ class UriHelper { } } } else { - let columns = [android.provider.MediaStore.MediaColumns.DATA]; + const columns = [android.provider.MediaStore.MediaColumns.DATA]; let filePath; try { cursor = this.getContentResolver().query(uri, columns, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { - let column_index = cursor.getColumnIndexOrThrow(columns[0]); + const column_index = cursor.getColumnIndexOrThrow(columns[0]); filePath = cursor.getString(column_index); if (filePath) { return filePath; @@ -151,7 +151,7 @@ class UriHelper { } } -export class ImagePicker { +export class ImagePicker implements ImagePickerApi { private _options: Options; constructor(options: Options) { @@ -176,8 +176,8 @@ export class ImagePicker { } get mimeTypes() { - let length = this.mediaType === '*/*' ? 2 : 1; - let mimeTypes = Array.create(java.lang.String, length); + const length = this.mediaType === '*/*' ? 2 : 1; + const mimeTypes = Array.create(java.lang.String, length); if (this.mediaType === '*/*') { mimeTypes[0] = 'image/*'; @@ -189,8 +189,8 @@ export class ImagePicker { } authorize(): Promise { + let requested: { [key: string]: permissions.PermissionOptions } = {}; if ((android).os.Build.VERSION.SDK_INT >= 33 && Utils.ad.getApplicationContext().getApplicationInfo().targetSdkVersion >= 33) { - let requested: { [key: string]: permissions.PermissionOptions } = {}; const mediaPerms = { photo: { reason: 'To pick images from your gallery' }, video: { reason: 'To pick videos from your gallery' }, @@ -205,7 +205,8 @@ export class ImagePicker { return permissions.request(requested); } else if ((android).os.Build.VERSION.SDK_INT >= 23) { - return permissions.request('storage', { read: true }); + requested['storage'] = { read: true, write: false }; + return permissions.request(requested); } else { return Promise.resolve({ storage: 'authorized' }); } diff --git a/packages/imagepicker/index.d.ts b/packages/imagepicker/index.d.ts index d76e0018..cd933da3 100644 --- a/packages/imagepicker/index.d.ts +++ b/packages/imagepicker/index.d.ts @@ -1,4 +1,4 @@ -import { Observable, ImageSource, ImageAsset, View } from '@nativescript/core'; +import { ImageSource, ImageAsset, View } from '@nativescript/core'; export class ImagePicker { constructor(options?: Options); diff --git a/packages/imagepicker/index.ios.ts b/packages/imagepicker/index.ios.ts index ba8099a3..cd08bc0c 100644 --- a/packages/imagepicker/index.ios.ts +++ b/packages/imagepicker/index.ios.ts @@ -1,5 +1,5 @@ -import { Observable, ImageAsset, View, Utils, Application, path, knownFolders, Folder, File, ImageSource } from '@nativescript/core'; -import { Options, ImagePickerMediaType } from './common'; +import { ImageAsset, View, Utils, Application, path, knownFolders, ImageSource } from '@nativescript/core'; +import { Options } from './common'; import { getFile } from '@nativescript/core/http'; export * from './common'; @@ -7,7 +7,7 @@ const defaultAssetCollectionSubtypes: NSArray = NSArray.arrayWithArray(options.mediaType.valueOf() : QBImagePickerMediaType.Any; @@ -50,7 +48,7 @@ export class ImagePicker extends Observable { console.log('authorizing...'); return new Promise((resolve, reject) => { - let runloop = CFRunLoopGetCurrent(); + const runloop = CFRunLoopGetCurrent(); PHPhotoLibrary.requestAuthorization(function (result) { if (result === PHAuthorizationStatus.Authorized) { resolve(); @@ -99,11 +97,11 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon qb_imagePickerControllerDidFinishPickingAssets?(imagePickerController: QBImagePickerController, iosAssets: NSArray): void { for (let i = 0; i < iosAssets.count; i++) { const asset = new ImageAsset(iosAssets.objectAtIndex(i)); - let phAssetImage: PHAsset = (asset)._ios; + const phAssetImage: PHAsset = (asset)._ios; // this fixes the image aspect ratio in tns-core-modules version < 4.0 if (!asset.options) asset.options = { keepAspectRatio: true }; - let existingFileName = phAssetImage.valueForKey('filename'); - let pickerSelection: any = { + const existingFileName = phAssetImage.valueForKey('filename'); + const pickerSelection: any = { asset: asset, type: phAssetImage.mediaType == 2 ? 'video' : 'image', filename: existingFileName, @@ -112,14 +110,14 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon if (pickerSelection.type == 'video') pickerSelection.duration = parseInt(phAssetImage.duration.toFixed(0)); fileMap[existingFileName] = pickerSelection; if (pickerSelection.type == 'video') { - let manager = new PHImageManager(); - let options = new PHVideoRequestOptions(); + const manager = new PHImageManager(); + const options = new PHVideoRequestOptions(); options.networkAccessAllowed = true; manager.requestAVAssetForVideoOptionsResultHandler(phAssetImage, options, (urlAsset: AVURLAsset, audioMix, info) => { fileMap[existingFileName].path = urlAsset.URL.toString().replace('file://', ''); }); } else { - let imageOptions = new PHContentEditingInputRequestOptions(); + const imageOptions = new PHContentEditingInputRequestOptions(); imageOptions.networkAccessAllowed = true; phAssetImage.requestContentEditingInputWithOptionsCompletionHandler(imageOptions, (thing) => { fileMap[existingFileName].path = thing.fullSizeImageURL.toString().replace('file://', ''); @@ -129,16 +127,16 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon if (this._resolve) { setTimeout(() => { - let promises = []; + const promises = []; let count = 0; - for (var key in fileMap) { - let item = fileMap[key]; + for (const key in fileMap) { + const item = fileMap[key]; const folder = knownFolders.documents(); - let extension = item.filename.split('.').pop(); + const extension = item.filename.split('.').pop(); let filename = renameFileTo ? renameFileTo + '.' + extension : item.filename; if (iosAssets.count > 1) filename = renameFileTo ? renameFileTo + '-' + count + '.' + extension : item.filename; fileMap[item.filename].filename = filename; - let fileManager = new NSFileManager(); + const fileManager = new NSFileManager(); if (copyToAppFolder) { const filePath = path.join(folder.path + '/' + copyToAppFolder, filename); promises.push( @@ -170,8 +168,8 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon } Promise.all(promises).then(() => { - let results = []; - for (var key in fileMap) { + const results = []; + for (const key in fileMap) { results.push(fileMap[key]); } this._resolve(results); diff --git a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml index 15375880..2cb2a37b 100644 --- a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml +++ b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml @@ -12,7 +12,6 @@ android:xlargeScreens="true"/> - diff --git a/tools/demo/imagepicker/index.ts b/tools/demo/imagepicker/index.ts index a3250060..6e79ffa3 100644 --- a/tools/demo/imagepicker/index.ts +++ b/tools/demo/imagepicker/index.ts @@ -85,7 +85,8 @@ export class DemoSharedImagepicker extends DemoSharedBase { private startSelection(context) { context .authorize() - .then(() => { + .then((authResult) => { + console.log(authResult); this.imageAssets = []; this.imageSrc = null; this.selection = null; From cd372b262a99367fc333a65faea5073c1177ccf3 Mon Sep 17 00:00:00 2001 From: Jason Cassidy <47318351+jcassidyav@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:25:29 +0100 Subject: [PATCH 2/4] fix(imagePicker): consolidate API --- .../plugin-demos/imagepicker.component.html | 2 +- .../src/plugin-demos/imagepicker.component.ts | 46 ++++++++-------- packages/imagepicker/README.md | 38 +++++++------ packages/imagepicker/common.ts | 32 +++++++++-- packages/imagepicker/index.android.ts | 23 ++++---- packages/imagepicker/index.d.ts | 6 +-- packages/imagepicker/index.ios.ts | 36 ++++++------- .../Android/src/main/AndroidManifest.xml | 2 +- tools/demo/imagepicker/index.ts | 53 ++++++++++--------- 9 files changed, 137 insertions(+), 101 deletions(-) diff --git a/apps/demo-angular/src/plugin-demos/imagepicker.component.html b/apps/demo-angular/src/plugin-demos/imagepicker.component.html index 31dbee8c..b28808e4 100644 --- a/apps/demo-angular/src/plugin-demos/imagepicker.component.html +++ b/apps/demo-angular/src/plugin-demos/imagepicker.component.html @@ -5,7 +5,7 @@ - + diff --git a/apps/demo-angular/src/plugin-demos/imagepicker.component.ts b/apps/demo-angular/src/plugin-demos/imagepicker.component.ts index 72480a7b..493b611d 100644 --- a/apps/demo-angular/src/plugin-demos/imagepicker.component.ts +++ b/apps/demo-angular/src/plugin-demos/imagepicker.component.ts @@ -1,14 +1,14 @@ import { Component, NgZone } from '@angular/core'; -import { ImageAsset } from '@nativescript/core'; -import * as imagepicker from '@nativescript/imagepicker'; +import { ImageAsset, ImageSource } from '@nativescript/core'; +import { ImagePicker, create, ImagePickerSelection } from '@nativescript/imagepicker'; @Component({ selector: 'demo-imagepicker', templateUrl: 'imagepicker.component.html', }) export class ImagepickerComponent { - imageAssets = []; - imageSrc: any; + imageAssets: ImagePickerSelection[] = []; + imageSrc: ImageAsset | ImageSource; isSingleMode: boolean = true; thumbSize: number = 80; previewSize: number = 300; @@ -18,7 +18,7 @@ export class ImagepickerComponent { public onSelectMultipleTap() { this.isSingleMode = false; - let context = imagepicker.create({ + let context = create({ mode: 'multiple', }); this.startSelection(context); @@ -27,36 +27,40 @@ export class ImagepickerComponent { public onSelectSingleTap() { this.isSingleMode = true; - let context = imagepicker.create({ + let context = create({ mode: 'single', }); this.startSelection(context); } - private startSelection(context) { + private startSelection(context: ImagePicker) { context .authorize() - .then(() => { + .then((authResult) => { this._ngZone.run(() => { this.imageAssets = []; this.imageSrc = null; }); - return context.present(); - }) - .then((selection) => { - this._ngZone.run(() => { - console.log('Selection done: ' + JSON.stringify(selection)); - this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0] : null; + if (authResult.authorized) { + return context.present().then((selection) => { + this._ngZone.run(() => { + console.log('Selection done: ' + JSON.stringify(selection)); + this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0].asset : null; - // set the images to be loaded from the assets with optimal sizes (optimize memory usage) - selection.forEach((el: ImageAsset) => { - el.options.width = this.isSingleMode ? this.previewSize : this.thumbSize; - el.options.height = this.isSingleMode ? this.previewSize : this.thumbSize; - }); + // set the images to be loaded from the assets with optimal sizes (optimize memory usage) + selection.forEach((el) => { + el.asset.options.width = this.isSingleMode ? this.previewSize : this.thumbSize; + el.asset.options.height = this.isSingleMode ? this.previewSize : this.thumbSize; + }); - this.imageAssets = selection; - }); + this.imageAssets = selection; + }); + }); + } else { + console.log('Unauthorised'); + } }) + .catch(function (e) { console.log(e); }); diff --git a/packages/imagepicker/README.md b/packages/imagepicker/README.md index 27458c27..a56bc63b 100644 --- a/packages/imagepicker/README.md +++ b/packages/imagepicker/README.md @@ -29,6 +29,11 @@ Install the plugin by running the following command in the root directory of you npm install @nativescript/imagepicker ``` +**Note: Version 3.0 contains breaking changes.** +* authorise() now returns a `Promise` for both android and ios. +* In the returned result from `present()` each `result[i].thumbnail` is now an ImageSource. +* `result[i].duration` is not typed correctly as a `number`. + **Note: Version 2.0 contains breaking changes. In order supply more information about your selection, the ImageSource asset is nested in the response so you'll need to update your code to use `result.asset` instead of `result` as your src for your Images.** ## Android required permissions @@ -97,22 +102,25 @@ The `present` method resolves with the selected media assets that can you to pro ```ts imagePickerObj .authorize() - .then(function() { - return imagePickerObj.present(); - }) - .then(function(selection) { - selection.forEach(function(selected) { - this.imageSource = selected.asset; - this.type = selected.type; - this.filesize = selected.filesize; - //etc - }); - list.items = selection; - }).catch(function (e) { + .then((authResult) => { + if(authResult.authorized) { + return imagePickerObj.present() + .then(function(selection) { + selection.forEach(function(selected) { + this.imageSource = selected.asset; + this.type = selected.type; + this.filesize = selected.filesize; + //etc + }); + }); + } else { + // process authorization not granted. + } + }) + .catch(function (e) { // process error }); ``` -> **Note** To request permissions for Android 6+ (API 23+), use [nativescript-permissions](https://www.npmjs.com/package/nativescript-permissions) plugin. ### Demo You can play with the plugin on StackBlitz at any of the following links: @@ -131,8 +139,8 @@ The class that provides the media selection API. It offers the following methods | Method | Returns | Description |:-------|:--------|:----------- | `constructor(options: Options)` | `ImagePicker` | Instanciates the ImagePicker class with the optional `options` parameter. See [Options](#options) -| `authorize()` | `Promise` | Requests the required permissions. Call it before calling `present()`. In case of a failed authorization, consider notifying the user for degraded functionality. -| `present()` | `Promise` | Presents the image picker UI. +| `authorize()` | `Promise` | Requests the required permissions. Call it before calling `present()`. In case of a failed authorization, consider notifying the user for degraded functionality. The returned `AuthorizationResult` will have it's `authorized` property to `true` if permission was granted. +| `present()` | `Promise` | Presents the image picker UI. | `create(options: Options, hostView: View)` | `ImagePicker` | Creates an instance of the ImagePicker class. The `hostView` parameter can be set to the view that hosts the image picker. Intended to be used when opening the picker from a modal page. ### Options diff --git a/packages/imagepicker/common.ts b/packages/imagepicker/common.ts index 59513e36..b4bc731d 100644 --- a/packages/imagepicker/common.ts +++ b/packages/imagepicker/common.ts @@ -1,4 +1,4 @@ -import { ImageAsset } from '@nativescript/core'; +import { ImageAsset, ImageSource } from '@nativescript/core'; import { MultiResult, Result } from '@nativescript-community/perms'; export enum ImagePickerMediaType { @@ -41,12 +41,12 @@ export interface ImagePickerSelection { /** * The duration of the video. Only passed if type is 'video' */ - duration?: string; + duration?: number; /** * An image to use for the video thumbnail. Only passed if type is 'video' */ - thumbnail?: ImageAsset; + thumbnail?: ImageSource; } /** @@ -133,6 +133,30 @@ export interface ImagePickerApi { } export interface AuthorizationResult { - authorised: boolean; + authorized: boolean; details: MultiResult | Result; } +const requestingPermissions = ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO']; + +export abstract class ImagePickerBase implements ImagePickerApi { + abstract authorize(): Promise; + abstract present(): Promise; + protected mapResult(result: MultiResult | Result): AuthorizationResult { + let authorized = true; + if (Array.isArray(result) && result.length == 2) { + // is of type Result + authorized = result[0] === 'authorized'; + } else { + const t = result as MultiResult; + requestingPermissions.forEach((permission) => { + if (t[permission] !== undefined) { + authorized = authorized && t[permission] === 'authorized'; + } + }); + } + return { + details: result, + authorized, + }; + } +} diff --git a/packages/imagepicker/index.android.ts b/packages/imagepicker/index.android.ts index 7a2386e8..8da55bea 100644 --- a/packages/imagepicker/index.android.ts +++ b/packages/imagepicker/index.android.ts @@ -1,7 +1,7 @@ -import { ImageAsset, Application, AndroidApplication, Utils, File, knownFolders } from '@nativescript/core'; +import { ImageAsset, Application, AndroidApplication, Utils, File, knownFolders, ImageSource } from '@nativescript/core'; import * as permissions from '@nativescript-community/perms'; -import { ImagePickerMediaType, Options, ImagePickerApi } from './common'; +import { ImagePickerMediaType, Options, AuthorizationResult, ImagePickerBase, ImagePickerSelection } from './common'; export * from './common'; let copyToAppFolder; let renameFileTo; @@ -151,10 +151,11 @@ class UriHelper { } } -export class ImagePicker implements ImagePickerApi { +export class ImagePicker extends ImagePickerBase { private _options: Options; constructor(options: Options) { + super(); this._options = options; copyToAppFolder = options.copyToAppFolder; renameFileTo = options.renameFileTo; @@ -188,7 +189,7 @@ export class ImagePicker implements ImagePickerApi { return mimeTypes; } - authorize(): Promise { + authorize(): Promise { let requested: { [key: string]: permissions.PermissionOptions } = {}; if ((android).os.Build.VERSION.SDK_INT >= 33 && Utils.ad.getApplicationContext().getApplicationInfo().targetSdkVersion >= 33) { const mediaPerms = { @@ -203,16 +204,16 @@ export class ImagePicker implements ImagePickerApi { requested = mediaPerms; } - return permissions.request(requested); + return permissions.request(requested).then((result) => this.mapResult(result)); } else if ((android).os.Build.VERSION.SDK_INT >= 23) { requested['storage'] = { read: true, write: false }; - return permissions.request(requested); + return permissions.request(requested).then((result) => this.mapResult(result)); } else { - return Promise.resolve({ storage: 'authorized' }); + return Promise.resolve({ details: null, authorized: true }); } } - present(): Promise { + present(): Promise { return new Promise((resolve, reject) => { // WARNING: If we want to support multiple pickers we will need to have a range of IDs here: let RESULT_CODE_PICKER_IMAGES = 9192; @@ -228,7 +229,7 @@ export class ImagePicker implements ImagePickerApi { const file = File.fromPath(selectedAsset.android); let copiedFile: any = false; - let item: any = { + const item: ImagePickerSelection = { asset: selectedAsset, filename: file.name, originalFilename: file.name, @@ -255,10 +256,10 @@ export class ImagePicker implements ImagePickerApi { item.filesize = new java.io.File(item.path).length(); } if (item.type == 'video') { - let thumb = android.media.ThumbnailUtils.createVideoThumbnail(copiedFile ? copiedFile.path : file.path, android.provider.MediaStore.Video.Thumbnails.MINI_KIND); + const thumb = android.media.ThumbnailUtils.createVideoThumbnail(copiedFile ? copiedFile.path : file.path, android.provider.MediaStore.Video.Thumbnails.MINI_KIND); let retriever = new android.media.MediaMetadataRetriever(); retriever.setDataSource(item.path); - item.thumbnail = thumb; + item.thumbnail = new ImageSource(thumb); let time = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_DURATION); let duration = parseInt(time) / 1000; item.duration = duration; diff --git a/packages/imagepicker/index.d.ts b/packages/imagepicker/index.d.ts index cd933da3..a6207715 100644 --- a/packages/imagepicker/index.d.ts +++ b/packages/imagepicker/index.d.ts @@ -1,13 +1,13 @@ import { ImageSource, ImageAsset, View } from '@nativescript/core'; - -export class ImagePicker { +import { AuthorizationResult, ImagePickerApi } from './common'; +export class ImagePicker implements ImagePickerApi { constructor(options?: Options); /** * Call this before 'present' to request any additional permissions that may be necessary. * In case of failed authorization consider notifying the user for degraded functionality. */ - authorize(): Promise; + authorize(): Promise; /** * Present the image picker UI. diff --git a/packages/imagepicker/index.ios.ts b/packages/imagepicker/index.ios.ts index cd08bc0c..431a57db 100644 --- a/packages/imagepicker/index.ios.ts +++ b/packages/imagepicker/index.ios.ts @@ -1,13 +1,16 @@ import { ImageAsset, View, Utils, Application, path, knownFolders, ImageSource } from '@nativescript/core'; -import { Options } from './common'; +import { AuthorizationResult, ImagePickerBase, ImagePickerSelection, Options } from './common'; import { getFile } from '@nativescript/core/http'; +import * as permissions from '@nativescript-community/perms'; export * from './common'; - +type FileMap = { + [key: string]: ImagePickerSelection; +}; const defaultAssetCollectionSubtypes: NSArray = NSArray.arrayWithArray([PHAssetCollectionSubtype.SmartAlbumRecentlyAdded, PHAssetCollectionSubtype.SmartAlbumUserLibrary, PHAssetCollectionSubtype.AlbumMyPhotoStream, PHAssetCollectionSubtype.SmartAlbumFavorites, PHAssetCollectionSubtype.SmartAlbumPanoramas, PHAssetCollectionSubtype.SmartAlbumBursts, PHAssetCollectionSubtype.AlbumCloudShared, PHAssetCollectionSubtype.SmartAlbumSelfPortraits, PHAssetCollectionSubtype.SmartAlbumScreenshots, PHAssetCollectionSubtype.SmartAlbumLivePhotos]); let copyToAppFolder; let renameFileTo; -let fileMap = {}; -export class ImagePicker { +let fileMap: FileMap = {}; +export class ImagePicker extends ImagePickerBase { _imagePickerController: QBImagePickerController; _hostView: View; _delegate: ImagePickerControllerDelegate; @@ -26,6 +29,7 @@ export class ImagePicker { } constructor(options: Options = {}, hostView: View) { + super(); this._hostView = hostView; const imagePickerController = QBImagePickerController.alloc().init(); @@ -44,24 +48,14 @@ export class ImagePicker { this._imagePickerController = imagePickerController; } - authorize(): Promise { + authorize(): Promise { console.log('authorizing...'); - - return new Promise((resolve, reject) => { - const runloop = CFRunLoopGetCurrent(); - PHPhotoLibrary.requestAuthorization(function (result) { - if (result === PHAuthorizationStatus.Authorized) { - resolve(); - } else { - reject(new Error('Authorization failed. Status: ' + result)); - } - }); - }); + return permissions.request('photo').then((result) => this.mapResult(result)); } - present() { + present(): Promise { fileMap = {}; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this._delegate = ImagePickerControllerDelegate.initWithOwner(this, resolve, reject); this._imagePickerController.delegate = this._delegate; @@ -101,11 +95,13 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon // this fixes the image aspect ratio in tns-core-modules version < 4.0 if (!asset.options) asset.options = { keepAspectRatio: true }; const existingFileName = phAssetImage.valueForKey('filename'); - const pickerSelection: any = { + const pickerSelection: ImagePickerSelection = { asset: asset, type: phAssetImage.mediaType == 2 ? 'video' : 'image', filename: existingFileName, originalFilename: existingFileName, + filesize: 0, + path: '', }; if (pickerSelection.type == 'video') pickerSelection.duration = parseInt(phAssetImage.duration.toFixed(0)); fileMap[existingFileName] = pickerSelection; @@ -168,7 +164,7 @@ class ImagePickerControllerDelegate extends NSObject implements QBImagePickerCon } Promise.all(promises).then(() => { - const results = []; + const results: ImagePickerSelection[] = []; for (const key in fileMap) { results.push(fileMap[key]); } diff --git a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml index 2cb2a37b..6281e8b6 100644 --- a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml +++ b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:largeScreens="true" android:xlargeScreens="true"/> - + diff --git a/tools/demo/imagepicker/index.ts b/tools/demo/imagepicker/index.ts index 6e79ffa3..dd02873c 100644 --- a/tools/demo/imagepicker/index.ts +++ b/tools/demo/imagepicker/index.ts @@ -1,10 +1,10 @@ import { DemoSharedBase } from '../utils'; import * as imagepicker from '@nativescript/imagepicker'; -import { ItemEventData, Label } from '@nativescript/core'; +import { ImageAsset, ImageSource, ItemEventData, Label } from '@nativescript/core'; export class DemoSharedImagepicker extends DemoSharedBase { private _selection: any; - private _imageSrc: any; + private _imageSrc: ImageSource | ImageAsset; private _imageAssets: Array; private _isSingleMode: boolean; @@ -16,11 +16,11 @@ export class DemoSharedImagepicker extends DemoSharedBase { return 300; } - get imageSrc(): any { + get imageSrc(): ImageSource | ImageAsset { return this._imageSrc; } - set imageSrc(value: any) { + set imageSrc(value: ImageSource | ImageAsset) { if (this._imageSrc !== value) { this._imageSrc = value; this.notifyPropertyChange('imageSrc', value); @@ -82,32 +82,35 @@ export class DemoSharedImagepicker extends DemoSharedBase { this.startSelection(context); } - private startSelection(context) { + private startSelection(context: imagepicker.ImagePicker) { context .authorize() .then((authResult) => { console.log(authResult); - this.imageAssets = []; - this.imageSrc = null; - this.selection = null; - return context.present(); - }) - .then((selection: imagepicker.ImagePickerSelection[]) => { - console.log('Selection done: ', selection); - this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0].asset : null; - if (selection[0].thumbnail) { - this.imageSrc = selection[0].thumbnail; + if (authResult.authorized) { + this.imageAssets = []; + this.imageSrc = null; + this.selection = null; + return context.present().then((selection: imagepicker.ImagePickerSelection[]) => { + console.log('Selection done: ', selection); + this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0].asset : null; + if (selection[0].thumbnail) { + this.imageSrc = selection[0].thumbnail; + } + this.selection = this.isSingleMode && selection.length > 0 ? selection[0] : null; + + // set the images to be loaded from the assets with optimal sizes (optimize memory usage) + selection.forEach((element) => { + let asset = element.asset; + asset.options.width = this.isSingleMode ? this.previewSize : this.thumbSize; + asset.options.height = this.isSingleMode ? this.previewSize : this.thumbSize; + }); + + this.imageAssets = selection; + }); + } else { + console.log('UnAuthorized'); } - this.selection = this.isSingleMode && selection.length > 0 ? selection[0] : null; - - // set the images to be loaded from the assets with optimal sizes (optimize memory usage) - selection.forEach((element) => { - let asset = element.asset; - asset.options.width = this.isSingleMode ? this.previewSize : this.thumbSize; - asset.options.height = this.isSingleMode ? this.previewSize : this.thumbSize; - }); - - this.imageAssets = selection; }) .catch(function (e) { console.log('selection error', e); From feac5b4a35b9bd7686fb84e7af334a824f538f61 Mon Sep 17 00:00:00 2001 From: Jason Cassidy <47318351+jcassidyav@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:36:51 +0100 Subject: [PATCH 3/4] chore(ImagePicker): Typos in readme --- packages/imagepicker/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/imagepicker/README.md b/packages/imagepicker/README.md index a56bc63b..78e04871 100644 --- a/packages/imagepicker/README.md +++ b/packages/imagepicker/README.md @@ -29,10 +29,10 @@ Install the plugin by running the following command in the root directory of you npm install @nativescript/imagepicker ``` -**Note: Version 3.0 contains breaking changes.** +**Note: Version 3.0 contains breaking changes:** * authorise() now returns a `Promise` for both android and ios. -* In the returned result from `present()` each `result[i].thumbnail` is now an ImageSource. -* `result[i].duration` is not typed correctly as a `number`. +* In the returned result from `present()` each `result[i].thumbnail` is now an `ImageSource`. +* `result[i].duration` is now typed correctly as a `number`. **Note: Version 2.0 contains breaking changes. In order supply more information about your selection, the ImageSource asset is nested in the response so you'll need to update your code to use `result.asset` instead of `result` as your src for your Images.** @@ -139,7 +139,7 @@ The class that provides the media selection API. It offers the following methods | Method | Returns | Description |:-------|:--------|:----------- | `constructor(options: Options)` | `ImagePicker` | Instanciates the ImagePicker class with the optional `options` parameter. See [Options](#options) -| `authorize()` | `Promise` | Requests the required permissions. Call it before calling `present()`. In case of a failed authorization, consider notifying the user for degraded functionality. The returned `AuthorizationResult` will have it's `authorized` property to `true` if permission was granted. +| `authorize()` | `Promise` | Requests the required permissions. Call it before calling `present()`. In case of a failed authorization, consider notifying the user for degraded functionality. The returned `AuthorizationResult` will have it's `authorized` property set to `true` if permission has been granted. | `present()` | `Promise` | Presents the image picker UI. | `create(options: Options, hostView: View)` | `ImagePicker` | Creates an instance of the ImagePicker class. The `hostView` parameter can be set to the view that hosts the image picker. Intended to be used when opening the picker from a modal page. From 7c5f7667de09d19bf2afcdeda1371beafe8ed245 Mon Sep 17 00:00:00 2001 From: Jason Cassidy <47318351+jcassidyav@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:56:08 +0100 Subject: [PATCH 4/4] chore(imagepicker): typo in README --- packages/imagepicker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/imagepicker/README.md b/packages/imagepicker/README.md index 78e04871..65cf9327 100644 --- a/packages/imagepicker/README.md +++ b/packages/imagepicker/README.md @@ -30,7 +30,7 @@ npm install @nativescript/imagepicker ``` **Note: Version 3.0 contains breaking changes:** -* authorise() now returns a `Promise` for both android and ios. +* authorize() now returns a `Promise` for both android and ios. * In the returned result from `present()` each `result[i].thumbnail` is now an `ImageSource`. * `result[i].duration` is now typed correctly as a `number`.