Skip to content

Commit 938d880

Browse files
authored
feat(imagepicker): consolidate API with improved typings (#530)
1 parent c39be93 commit 938d880

File tree

9 files changed

+187
-132
lines changed

9 files changed

+187
-132
lines changed

Diff for: apps/demo-angular/src/plugin-demos/imagepicker.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ListView [items]="imageAssets" *ngIf="!isSingleMode">
66
<ng-template let-image="item" let-i="index">
77
<GridLayout columns="auto, *">
8-
<Image [width]="thumbSize" [height]="thumbSize" [src]="image" stretch="aspectFill"></Image>
8+
<Image [width]="thumbSize" [height]="thumbSize" [src]="image.asset" stretch="aspectFill"></Image>
99
<Label col="1" [text]="'image ' + i"></Label>
1010
</GridLayout>
1111
</ng-template>

Diff for: apps/demo-angular/src/plugin-demos/imagepicker.component.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Component, NgZone } from '@angular/core';
2-
import { ImageAsset } from '@nativescript/core';
3-
import * as imagepicker from '@nativescript/imagepicker';
2+
import { ImageAsset, ImageSource } from '@nativescript/core';
3+
import { ImagePicker, create, ImagePickerSelection } from '@nativescript/imagepicker';
44

55
@Component({
66
selector: 'demo-imagepicker',
77
templateUrl: 'imagepicker.component.html',
88
})
99
export class ImagepickerComponent {
10-
imageAssets = [];
11-
imageSrc: any;
10+
imageAssets: ImagePickerSelection[] = [];
11+
imageSrc: ImageAsset | ImageSource;
1212
isSingleMode: boolean = true;
1313
thumbSize: number = 80;
1414
previewSize: number = 300;
@@ -18,7 +18,7 @@ export class ImagepickerComponent {
1818
public onSelectMultipleTap() {
1919
this.isSingleMode = false;
2020

21-
let context = imagepicker.create({
21+
let context = create({
2222
mode: 'multiple',
2323
});
2424
this.startSelection(context);
@@ -27,36 +27,40 @@ export class ImagepickerComponent {
2727
public onSelectSingleTap() {
2828
this.isSingleMode = true;
2929

30-
let context = imagepicker.create({
30+
let context = create({
3131
mode: 'single',
3232
});
3333
this.startSelection(context);
3434
}
3535

36-
private startSelection(context) {
36+
private startSelection(context: ImagePicker) {
3737
context
3838
.authorize()
39-
.then(() => {
39+
.then((authResult) => {
4040
this._ngZone.run(() => {
4141
this.imageAssets = [];
4242
this.imageSrc = null;
4343
});
44-
return context.present();
45-
})
46-
.then((selection) => {
47-
this._ngZone.run(() => {
48-
console.log('Selection done: ' + JSON.stringify(selection));
49-
this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0] : null;
44+
if (authResult.authorized) {
45+
return context.present().then((selection) => {
46+
this._ngZone.run(() => {
47+
console.log('Selection done: ' + JSON.stringify(selection));
48+
this.imageSrc = this.isSingleMode && selection.length > 0 ? selection[0].asset : null;
5049

51-
// set the images to be loaded from the assets with optimal sizes (optimize memory usage)
52-
selection.forEach((el: ImageAsset) => {
53-
el.options.width = this.isSingleMode ? this.previewSize : this.thumbSize;
54-
el.options.height = this.isSingleMode ? this.previewSize : this.thumbSize;
55-
});
50+
// set the images to be loaded from the assets with optimal sizes (optimize memory usage)
51+
selection.forEach((el) => {
52+
el.asset.options.width = this.isSingleMode ? this.previewSize : this.thumbSize;
53+
el.asset.options.height = this.isSingleMode ? this.previewSize : this.thumbSize;
54+
});
5655

57-
this.imageAssets = selection;
58-
});
56+
this.imageAssets = selection;
57+
});
58+
});
59+
} else {
60+
console.log('Unauthorised');
61+
}
5962
})
63+
6064
.catch(function (e) {
6165
console.log(e);
6266
});

Diff for: packages/imagepicker/README.md

+23-15
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ Install the plugin by running the following command in the root directory of you
2929
npm install @nativescript/imagepicker
3030
```
3131

32+
**Note: Version 3.0 contains breaking changes:**
33+
* authorize() now returns a `Promise<AuthorizationResult>` for both android and ios.
34+
* In the returned result from `present()` each `result[i].thumbnail` is now an `ImageSource`.
35+
* `result[i].duration` is now typed correctly as a `number`.
36+
3237
**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.**
3338

3439
## Android required permissions
@@ -97,22 +102,25 @@ The `present` method resolves with the selected media assets that can you to pro
97102
```ts
98103
imagePickerObj
99104
.authorize()
100-
.then(function() {
101-
return imagePickerObj.present();
102-
})
103-
.then(function(selection) {
104-
selection.forEach(function(selected) {
105-
this.imageSource = selected.asset;
106-
this.type = selected.type;
107-
this.filesize = selected.filesize;
108-
//etc
109-
});
110-
list.items = selection;
111-
}).catch(function (e) {
105+
.then((authResult) => {
106+
if(authResult.authorized) {
107+
return imagePickerObj.present()
108+
.then(function(selection) {
109+
selection.forEach(function(selected) {
110+
this.imageSource = selected.asset;
111+
this.type = selected.type;
112+
this.filesize = selected.filesize;
113+
//etc
114+
});
115+
});
116+
} else {
117+
// process authorization not granted.
118+
}
119+
})
120+
.catch(function (e) {
112121
// process error
113122
});
114123
```
115-
> **Note** To request permissions for Android 6+ (API 23+), use [nativescript-permissions](https://www.npmjs.com/package/nativescript-permissions) plugin.
116124

117125
### Demo
118126
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
131139
| Method | Returns | Description
132140
|:-------|:--------|:-----------
133141
| `constructor(options: Options)` | `ImagePicker` | Instanciates the ImagePicker class with the optional `options` parameter. See [Options](#options)
134-
| `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.
135-
| `present()` | `Promise<ImageAsset[]>` | Presents the image picker UI.
142+
| `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.
143+
| `present()` | `Promise<ImagePickerSelection[]>` | Presents the image picker UI.
136144
| `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.
137145

138146
### Options

Diff for: packages/imagepicker/common.ts

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ImageAsset } from '@nativescript/core';
1+
import { ImageAsset, ImageSource } from '@nativescript/core';
2+
import { MultiResult, Result } from '@nativescript-community/perms';
23

34
export enum ImagePickerMediaType {
45
Any = 0,
@@ -40,12 +41,12 @@ export interface ImagePickerSelection {
4041
/**
4142
* The duration of the video. Only passed if type is 'video'
4243
*/
43-
duration?: string;
44+
duration?: number;
4445

4546
/**
4647
* An image to use for the video thumbnail. Only passed if type is 'video'
4748
*/
48-
thumbnail?: ImageAsset;
49+
thumbnail?: ImageSource;
4950
}
5051

5152
/**
@@ -116,3 +117,46 @@ export interface Options {
116117
read_external_storage?: string;
117118
};
118119
}
120+
121+
export interface ImagePickerApi {
122+
/**
123+
* Call this before 'present' to request any additional permissions that may be necessary.
124+
* In case of failed authorization consider notifying the user for degraded functionality.
125+
*/
126+
authorize(): Promise<AuthorizationResult>;
127+
128+
/**
129+
* Present the image picker UI.
130+
* The result will be an array of SelectedAsset instances provided when the promise is fulfilled.
131+
*/
132+
present(): Promise<ImagePickerSelection[]>;
133+
}
134+
135+
export interface AuthorizationResult {
136+
authorized: boolean;
137+
details: MultiResult | Result;
138+
}
139+
const requestingPermissions = ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO'];
140+
141+
export abstract class ImagePickerBase implements ImagePickerApi {
142+
abstract authorize(): Promise<AuthorizationResult>;
143+
abstract present(): Promise<ImagePickerSelection[]>;
144+
protected mapResult(result: MultiResult | Result): AuthorizationResult {
145+
let authorized = true;
146+
if (Array.isArray(result) && result.length == 2) {
147+
// is of type Result
148+
authorized = result[0] === 'authorized';
149+
} else {
150+
const t = result as MultiResult;
151+
requestingPermissions.forEach((permission) => {
152+
if (t[permission] !== undefined) {
153+
authorized = authorized && t[permission] === 'authorized';
154+
}
155+
});
156+
}
157+
return {
158+
details: result,
159+
authorized,
160+
};
161+
}
162+
}

Diff for: packages/imagepicker/index.android.ts

+28-26
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { ImageAsset, Application, AndroidApplication, Utils, File, knownFolders } from '@nativescript/core';
1+
import { ImageAsset, Application, AndroidApplication, Utils, File, knownFolders, ImageSource } from '@nativescript/core';
22
import * as permissions from '@nativescript-community/perms';
33

4-
import { ImagePickerMediaType, Options } from './common';
4+
import { ImagePickerMediaType, Options, AuthorizationResult, ImagePickerBase, ImagePickerSelection } from './common';
55
export * from './common';
66
let copyToAppFolder;
77
let renameFileTo;
8-
let fileMap = {};
9-
let videoFiles = {
8+
9+
const videoFiles = {
1010
mp4: true,
1111
mov: true,
1212
avi: true,
@@ -22,8 +22,8 @@ let videoFiles = {
2222
};
2323
class UriHelper {
2424
public static _calculateFileUri(uri: android.net.Uri) {
25-
let DocumentsContract = (<any>android.provider).DocumentsContract;
26-
let isKitKat = android.os.Build.VERSION.SDK_INT >= 19; // android.os.Build.VERSION_CODES.KITKAT
25+
const DocumentsContract = (<any>android.provider).DocumentsContract;
26+
const isKitKat = android.os.Build.VERSION.SDK_INT >= 19; // android.os.Build.VERSION_CODES.KITKAT
2727

2828
if (isKitKat && DocumentsContract.isDocumentUri(Utils.android.getApplicationContext(), uri)) {
2929
let docId, id, type;
@@ -56,7 +56,7 @@ class UriHelper {
5656
// MediaProvider
5757
else if (UriHelper.isMediaDocument(uri)) {
5858
docId = DocumentsContract.getDocumentId(uri);
59-
let split = docId.split(':');
59+
const split = docId.split(':');
6060
type = split[0];
6161
id = split[1];
6262

@@ -68,8 +68,8 @@ class UriHelper {
6868
contentUri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
6969
}
7070

71-
let selection = '_id=?';
72-
let selectionArgs = [id];
71+
const selection = '_id=?';
72+
const selectionArgs = [id];
7373

7474
return UriHelper.getDataColumn(contentUri, selection, selectionArgs, false);
7575
}
@@ -89,13 +89,13 @@ class UriHelper {
8989

9090
private static getDataColumn(uri: android.net.Uri, selection, selectionArgs, isDownload: boolean) {
9191
let cursor = null;
92-
let filePath;
92+
let filePath: string;
9393
if (isDownload) {
94-
let columns = ['_display_name'];
94+
const columns = ['_display_name'];
9595
try {
9696
cursor = this.getContentResolver().query(uri, columns, selection, selectionArgs, null);
9797
if (cursor != null && cursor.moveToFirst()) {
98-
let column_index = cursor.getColumnIndexOrThrow(columns[0]);
98+
const column_index = cursor.getColumnIndexOrThrow(columns[0]);
9999
filePath = cursor.getString(column_index);
100100
if (filePath) {
101101
const dl = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS);
@@ -111,13 +111,13 @@ class UriHelper {
111111
}
112112
}
113113
} else {
114-
let columns = [android.provider.MediaStore.MediaColumns.DATA];
114+
const columns = [android.provider.MediaStore.MediaColumns.DATA];
115115
let filePath;
116116

117117
try {
118118
cursor = this.getContentResolver().query(uri, columns, selection, selectionArgs, null);
119119
if (cursor != null && cursor.moveToFirst()) {
120-
let column_index = cursor.getColumnIndexOrThrow(columns[0]);
120+
const column_index = cursor.getColumnIndexOrThrow(columns[0]);
121121
filePath = cursor.getString(column_index);
122122
if (filePath) {
123123
return filePath;
@@ -151,10 +151,11 @@ class UriHelper {
151151
}
152152
}
153153

154-
export class ImagePicker {
154+
export class ImagePicker extends ImagePickerBase {
155155
private _options: Options;
156156

157157
constructor(options: Options) {
158+
super();
158159
this._options = options;
159160
copyToAppFolder = options.copyToAppFolder;
160161
renameFileTo = options.renameFileTo;
@@ -176,8 +177,8 @@ export class ImagePicker {
176177
}
177178

178179
get mimeTypes() {
179-
let length = this.mediaType === '*/*' ? 2 : 1;
180-
let mimeTypes = Array.create(java.lang.String, length);
180+
const length = this.mediaType === '*/*' ? 2 : 1;
181+
const mimeTypes = Array.create(java.lang.String, length);
181182

182183
if (this.mediaType === '*/*') {
183184
mimeTypes[0] = 'image/*';
@@ -188,9 +189,9 @@ export class ImagePicker {
188189
return mimeTypes;
189190
}
190191

191-
authorize(): Promise<permissions.MultiResult | permissions.Result> {
192+
authorize(): Promise<AuthorizationResult> {
193+
let requested: { [key: string]: permissions.PermissionOptions } = {};
192194
if ((<any>android).os.Build.VERSION.SDK_INT >= 33 && Utils.ad.getApplicationContext().getApplicationInfo().targetSdkVersion >= 33) {
193-
let requested: { [key: string]: permissions.PermissionOptions } = {};
194195
const mediaPerms = {
195196
photo: { reason: 'To pick images from your gallery' },
196197
video: { reason: 'To pick videos from your gallery' },
@@ -203,15 +204,16 @@ export class ImagePicker {
203204
requested = mediaPerms;
204205
}
205206

206-
return permissions.request(requested);
207+
return permissions.request(requested).then((result) => this.mapResult(result));
207208
} else if ((<any>android).os.Build.VERSION.SDK_INT >= 23) {
208-
return permissions.request('storage', { read: true });
209+
requested['storage'] = { read: true, write: false };
210+
return permissions.request(requested).then((result) => this.mapResult(result));
209211
} else {
210-
return Promise.resolve({ storage: 'authorized' });
212+
return Promise.resolve({ details: null, authorized: true });
211213
}
212214
}
213215

214-
present(): Promise<ImageAsset[]> {
216+
present(): Promise<ImagePickerSelection[]> {
215217
return new Promise((resolve, reject) => {
216218
// WARNING: If we want to support multiple pickers we will need to have a range of IDs here:
217219
let RESULT_CODE_PICKER_IMAGES = 9192;
@@ -227,7 +229,7 @@ export class ImagePicker {
227229
const file = File.fromPath(selectedAsset.android);
228230
let copiedFile: any = false;
229231

230-
let item: any = {
232+
const item: ImagePickerSelection = {
231233
asset: selectedAsset,
232234
filename: file.name,
233235
originalFilename: file.name,
@@ -254,10 +256,10 @@ export class ImagePicker {
254256
item.filesize = new java.io.File(item.path).length();
255257
}
256258
if (item.type == 'video') {
257-
let thumb = android.media.ThumbnailUtils.createVideoThumbnail(copiedFile ? copiedFile.path : file.path, android.provider.MediaStore.Video.Thumbnails.MINI_KIND);
259+
const thumb = android.media.ThumbnailUtils.createVideoThumbnail(copiedFile ? copiedFile.path : file.path, android.provider.MediaStore.Video.Thumbnails.MINI_KIND);
258260
let retriever = new android.media.MediaMetadataRetriever();
259261
retriever.setDataSource(item.path);
260-
item.thumbnail = thumb;
262+
item.thumbnail = new ImageSource(thumb);
261263
let time = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_DURATION);
262264
let duration = parseInt(time) / 1000;
263265
item.duration = duration;

0 commit comments

Comments
 (0)