Skip to content

fix(imagepicker): consolidate API / Fix typings #530

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ListView [items]="imageAssets" *ngIf="!isSingleMode">
<ng-template let-image="item" let-i="index">
<GridLayout columns="auto, *">
<Image [width]="thumbSize" [height]="thumbSize" [src]="image" stretch="aspectFill"></Image>
<Image [width]="thumbSize" [height]="thumbSize" [src]="image.asset" stretch="aspectFill"></Image>
<Label col="1" [text]="'image ' + i"></Label>
</GridLayout>
</ng-template>
Expand Down
46 changes: 25 additions & 21 deletions apps/demo-angular/src/plugin-demos/imagepicker.component.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +18,7 @@ export class ImagepickerComponent {
public onSelectMultipleTap() {
this.isSingleMode = false;

let context = imagepicker.create({
let context = create({
mode: 'multiple',
});
this.startSelection(context);
Expand All @@ -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);
});
Expand Down
38 changes: 23 additions & 15 deletions packages/imagepicker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
* authorize() now returns a `Promise<AuthorizationResult>` 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`.

**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
Expand Down Expand Up @@ -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:
Expand All @@ -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<void>` | Requests the required permissions. Call it before calling `present()`. In case of a failed authorization, consider notifying the user for degraded functionality.
| `present()` | `Promise<ImageAsset[]>` | Presents the image picker UI.
| `authorize()` | `Promise<AuthorizationResult>` | 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<ImagePickerSelection[]>` | 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
Expand Down
50 changes: 47 additions & 3 deletions packages/imagepicker/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ImageAsset } from '@nativescript/core';
import { ImageAsset, ImageSource } from '@nativescript/core';
import { MultiResult, Result } from '@nativescript-community/perms';

export enum ImagePickerMediaType {
Any = 0,
Expand Down Expand Up @@ -40,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;
}

/**
Expand Down Expand Up @@ -116,3 +117,46 @@ 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<AuthorizationResult>;

/**
* Present the image picker UI.
* The result will be an array of SelectedAsset instances provided when the promise is fulfilled.
*/
present(): Promise<ImagePickerSelection[]>;
}

export interface AuthorizationResult {
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<AuthorizationResult>;
abstract present(): Promise<ImagePickerSelection[]>;
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,
};
}
}
54 changes: 28 additions & 26 deletions packages/imagepicker/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 } from './common';
import { ImagePickerMediaType, Options, AuthorizationResult, ImagePickerBase, ImagePickerSelection } from './common';
export * from './common';
let copyToAppFolder;
let renameFileTo;
let fileMap = {};
let videoFiles = {

const videoFiles = {
mp4: true,
mov: true,
avi: true,
Expand All @@ -22,8 +22,8 @@ let videoFiles = {
};
class UriHelper {
public static _calculateFileUri(uri: android.net.Uri) {
let DocumentsContract = (<any>android.provider).DocumentsContract;
let isKitKat = android.os.Build.VERSION.SDK_INT >= 19; // android.os.Build.VERSION_CODES.KITKAT
const DocumentsContract = (<any>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;
Expand Down Expand Up @@ -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];

Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -151,10 +151,11 @@ class UriHelper {
}
}

export class ImagePicker {
export class ImagePicker extends ImagePickerBase {
private _options: Options;

constructor(options: Options) {
super();
this._options = options;
copyToAppFolder = options.copyToAppFolder;
renameFileTo = options.renameFileTo;
Expand All @@ -176,8 +177,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/*';
Expand All @@ -188,9 +189,9 @@ export class ImagePicker {
return mimeTypes;
}

authorize(): Promise<permissions.MultiResult | permissions.Result> {
authorize(): Promise<AuthorizationResult> {
let requested: { [key: string]: permissions.PermissionOptions } = {};
if ((<any>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' },
Expand All @@ -203,15 +204,16 @@ export class ImagePicker {
requested = mediaPerms;
}

return permissions.request(requested);
return permissions.request(requested).then((result) => this.mapResult(result));
} else if ((<any>android).os.Build.VERSION.SDK_INT >= 23) {
return permissions.request('storage', { read: true });
requested['storage'] = { read: true, write: false };
return permissions.request(requested).then((result) => this.mapResult(result));
} else {
return Promise.resolve({ storage: 'authorized' });
return Promise.resolve({ details: null, authorized: true });
}
}

present(): Promise<ImageAsset[]> {
present(): Promise<ImagePickerSelection[]> {
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;
Expand All @@ -227,7 +229,7 @@ export class ImagePicker {
const file = File.fromPath(selectedAsset.android);
let copiedFile: any = false;

let item: any = {
const item: ImagePickerSelection = {
asset: selectedAsset,
filename: file.name,
originalFilename: file.name,
Expand All @@ -254,10 +256,10 @@ export class ImagePicker {
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;
Expand Down
Loading