From 69cdaf681105073488c3fb58cdb9c7e4976aecc4 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Mon, 23 Mar 2026 18:12:09 -0400 Subject: [PATCH 01/29] feat: add web implementation for takePhoto and chooseFromGallery Context: This code was generated by Claude. --- src/web.ts | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 5 deletions(-) diff --git a/src/web.ts b/src/web.ts index 57f7047..0f6bc38 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,6 @@ import { WebPlugin, CapacitorException } from '@capacitor/core'; -import { CameraSource, CameraDirection } from './definitions'; +import { CameraSource, CameraDirection, MediaType, MediaTypeSelection } from './definitions'; import type { CameraPlugin, GalleryImageOptions, @@ -20,8 +20,15 @@ import type { } from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { - async takePhoto(_options: TakePhotoOptions): Promise { - throw this.unimplemented('takePhoto is not implemented on Web.'); + async takePhoto(options: TakePhotoOptions): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + if (options.webUseInput) { + this.takePhotoCameraInputExperience(options, resolve, reject); + } else { + this.takePhotoCameraExperience(options, resolve, reject); + } + }); } async recordVideo(_options: RecordVideoOptions): Promise { @@ -32,8 +39,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { throw this.unimplemented('playVideo is not implemented on Web.'); } - async chooseFromGallery(_options: ChooseFromGalleryOptions): Promise { - throw this.unimplemented('chooseFromGallery is not implemented on web.'); + async chooseFromGallery(options: ChooseFromGalleryOptions): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + this.galleryInputExperience(options, resolve, reject); + }); } async editPhoto(_options: EditPhotoOptions): Promise { @@ -268,6 +278,194 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { + if (customElements.get('pwa-camera-modal')) { + const cameraModal: any = document.createElement('pwa-camera-modal'); + cameraModal.facingMode = options.cameraDirection === CameraDirection.Front ? 'user' : 'environment'; + document.body.appendChild(cameraModal); + try { + await cameraModal.componentOnReady(); + cameraModal.addEventListener('onPhoto', async (e: any) => { + const photo = e.detail; + + if (photo === null) { + reject(new CapacitorException('User cancelled photos app')); + } else if (photo instanceof Error) { + reject(photo); + } else { + resolve(await this._getCameraPhotoAsMediaResult(photo)); + } + + cameraModal.dismiss(); + document.body.removeChild(cameraModal); + }); + + cameraModal.present(); + } catch (e) { + this.takePhotoCameraInputExperience(options, resolve, reject); + } + } else { + console.error( + `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, + ); + this.takePhotoCameraInputExperience(options, resolve, reject); + } + } + + private takePhotoCameraInputExperience(options: TakePhotoOptions, resolve: any, reject: any) { + let input = document.querySelector('#_capacitor-camera-input-takephoto') as HTMLInputElement; + + const cleanup = () => { + input.parentNode?.removeChild(input); + }; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = '_capacitor-camera-input-takephoto'; + input.type = 'file'; + input.hidden = true; + document.body.appendChild(input); + input.addEventListener('change', (_e: any) => { + const file = input.files![0]; + let format = 'jpeg'; + + if (file.type === 'image/png') { + format = 'png'; + } else if (file.type === 'image/gif') { + format = 'gif'; + } + + const reader = new FileReader(); + reader.addEventListener('load', () => { + const b64 = (reader.result as string).split(',')[1]; + resolve({ + type: MediaType.Photo, + thumbnail: b64, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from file input + }, + } as MediaResult); + cleanup(); + }); + + reader.readAsDataURL(file); + }); + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + + input.accept = 'image/*'; + (input as any).capture = true; + + if (options.cameraDirection === CameraDirection.Front) { + (input as any).capture = 'user'; + } else if (options.cameraDirection === CameraDirection.Rear) { + (input as any).capture = 'environment'; + } + + input.click(); + } + + private galleryInputExperience(options: ChooseFromGalleryOptions, resolve: any, reject: any) { + let input = document.querySelector('#_capacitor-camera-input-gallery') as HTMLInputElement; + + const cleanup = () => { + input.parentNode?.removeChild(input); + }; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = '_capacitor-camera-input-gallery'; + input.type = 'file'; + input.hidden = true; + input.multiple = options.allowMultipleSelection ?? false; + document.body.appendChild(input); + input.addEventListener('change', (_e: any) => { + const results: MediaResult[] = []; + const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; + const filesToProcess = Math.min(limit, input.files!.length); + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < filesToProcess; i++) { + const file = input.files![i]; + let format = 'jpeg'; + let type = MediaType.Photo; + + if (file.type.startsWith('image/')) { + type = MediaType.Photo; + if (file.type === 'image/png') { + format = 'png'; + } else if (file.type === 'image/gif') { + format = 'gif'; + } + } else if (file.type.startsWith('video/')) { + type = MediaType.Video; + format = file.type.split('/')[1]; + } + + results.push({ + type, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from file input + }, + }); + } + resolve({ results }); + cleanup(); + }); + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + + // Set accept attribute based on mediaType + const mediaType = options.mediaType ?? MediaTypeSelection.Photo; + if (mediaType === MediaTypeSelection.Photo) { + input.accept = 'image/*'; + } else if (mediaType === MediaTypeSelection.Video) { + input.accept = 'video/*'; + } else { + // MediaTypeSelection.All + input.accept = 'image/*,video/*'; + } + + input.click(); + } + + private _getCameraPhotoAsMediaResult(photo: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + const format = photo.type.split('/')[1]; + reader.readAsDataURL(photo); + reader.onloadend = () => { + const r = reader.result as string; + const b64 = r.split(',')[1]; + resolve({ + type: MediaType.Photo, + thumbnail: b64, + webPath: URL.createObjectURL(photo), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from blob + }, + }); + }; + reader.onerror = (e) => { + reject(e); + }; + }); + } + async checkPermissions(): Promise { if (typeof navigator === 'undefined' || !navigator.permissions) { throw this.unavailable('Permissions API not available in this browser'); From 6f2504b34670136c42b20ecc39c9603d254c2976 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 07:55:09 -0400 Subject: [PATCH 02/29] feat: get resolution for images and videos --- src/web.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/web.ts b/src/web.ts index 0f6bc38..549d67d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -325,7 +325,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.type = 'file'; input.hidden = true; document.body.appendChild(input); - input.addEventListener('change', (_e: any) => { + input.addEventListener('change', async (_e: any) => { const file = input.files![0]; let format = 'jpeg'; @@ -335,6 +335,9 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { format = 'gif'; } + // Get resolution from image file + const resolution = await this._getImageResolution(file); + const reader = new FileReader(); reader.addEventListener('load', () => { const b64 = (reader.result as string).split(',')[1]; @@ -345,7 +348,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from file input + resolution, }, } as MediaResult); cleanup(); @@ -385,7 +388,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.hidden = true; input.multiple = options.allowMultipleSelection ?? false; document.body.appendChild(input); - input.addEventListener('change', (_e: any) => { + input.addEventListener('change', async (_e: any) => { const results: MediaResult[] = []; const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; const filesToProcess = Math.min(limit, input.files!.length); @@ -395,6 +398,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![i]; let format = 'jpeg'; let type = MediaType.Photo; + let resolution = '0x0'; if (file.type.startsWith('image/')) { type = MediaType.Photo; @@ -403,9 +407,19 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } else if (file.type === 'image/gif') { format = 'gif'; } + + // Get resolution from image file + resolution = await this._getImageResolution(file); } else if (file.type.startsWith('video/')) { type = MediaType.Video; format = file.type.split('/')[1]; + + // Get resolution from video file + try { + resolution = await this._getVideoResolution(file); + } catch (e) { + console.warn('Failed to get video resolution:', e); + } } results.push({ @@ -414,7 +428,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from file input + resolution, }, }); } @@ -441,10 +455,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private _getCameraPhotoAsMediaResult(photo: Blob) { - return new Promise((resolve, reject) => { + private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { + return new Promise(async (resolve, reject) => { const reader = new FileReader(); const format = photo.type.split('/')[1]; + + // Get resolution from image blob + const resolution = await this._getImageResolution(photo); + reader.readAsDataURL(photo); reader.onloadend = () => { const r = reader.result as string; @@ -456,7 +474,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from blob + resolution, }, }); }; @@ -466,6 +484,40 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private async _getImageResolution(image: Blob | File): Promise { + try { + const bitmap = await createImageBitmap(image); + const resolution = `${bitmap.width}x${bitmap.height}`; + bitmap.close(); + return resolution; + } catch (e) { + console.warn('Failed to get image resolution:', e); + return '0x0'; + } + } + + private _getVideoResolution(videoFile: File): Promise { + return new Promise((resolve) => { + const video = document.createElement('video'); + video.preload = 'metadata'; + + video.onloadedmetadata = () => { + // Clean up + URL.revokeObjectURL(video.src); + const resolution = `${video.videoWidth}x${video.videoHeight}`; + resolve(resolution); + }; + + video.onerror = () => { + // Clean up and return default + URL.revokeObjectURL(video.src); + resolve('0x0'); + }; + + video.src = URL.createObjectURL(videoFile); + }); + } + async checkPermissions(): Promise { if (typeof navigator === 'undefined' || !navigator.permissions) { throw this.unavailable('Permissions API not available in this browser'); From ce960fe43eb3720cfff39925c2bcfdc96e2a2839 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 08:08:53 -0400 Subject: [PATCH 03/29] refactor: remove redundant assign --- src/web.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web.ts b/src/web.ts index 549d67d..5f9cf87 100644 --- a/src/web.ts +++ b/src/web.ts @@ -401,7 +401,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { let resolution = '0x0'; if (file.type.startsWith('image/')) { - type = MediaType.Photo; if (file.type === 'image/png') { format = 'png'; } else if (file.type === 'image/gif') { From 96e5abf7be418e010763a6bd995c51126ad71ce4 Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Tue, 24 Mar 2026 08:11:51 -0400 Subject: [PATCH 04/29] fix: use rear camera as default Co-authored-by: Pedro Bilro --- src/web.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/web.ts b/src/web.ts index 5f9cf87..19e8e49 100644 --- a/src/web.ts +++ b/src/web.ts @@ -363,11 +363,10 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } input.accept = 'image/*'; - (input as any).capture = true; - if (options.cameraDirection === CameraDirection.Front) { (input as any).capture = 'user'; - } else if (options.cameraDirection === CameraDirection.Rear) { + } else { + // CameraDirection.Rear (input as any).capture = 'environment'; } From a05e1df0ad0779be8bf1a0be2ba06695eec8b3ce Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 08:37:14 -0400 Subject: [PATCH 05/29] feat: add size, creationDate, and duration to new Web functions --- README.md | 6 +++--- src/definitions.ts | 5 +---- src/web.ts | 38 +++++++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f059a6a..ae19024 100644 --- a/README.md +++ b/README.md @@ -363,11 +363,11 @@ Allows the user to pick multiple pictures from the photo gallery. | Prop | Type | Description | Since | | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`size`** | number | File size of the media, in bytes Not available on Web. | 8.1.0 | -| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. Not available on Web. | 8.1.0 | +| **`size`** | number | File size of the media, in bytes. | 8.1.0 | +| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | | **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | | **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. Not available on web. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. | 8.1.0 | | **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 867de8f..6acd866 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -480,8 +480,7 @@ export interface MediaResult { export interface MediaMetadata { /** - * File size of the media, in bytes - * Not available on Web. + * File size of the media, in bytes. * * @since 8.1.0 */ @@ -489,7 +488,6 @@ export interface MediaMetadata { /** * Only applicable for `MediaType.Video` - the duration of the media, in seconds. - * Not available on Web. * * @since 8.1.0 */ @@ -515,7 +513,6 @@ export interface MediaMetadata { /** * The date and time the media was created, in ISO 8601 format. * If creation date is not available (e.g. Android 7 and below), the last modified date is returned. - * Not available on web. * * @since 8.1.0 */ diff --git a/src/web.ts b/src/web.ts index 19e8e49..70b98ca 100644 --- a/src/web.ts +++ b/src/web.ts @@ -349,6 +349,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), }, } as MediaResult); cleanup(); @@ -412,12 +414,29 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { type = MediaType.Video; format = file.type.split('/')[1]; - // Get resolution from video file + // Get resolution and duration from video file + let duration: number | undefined; try { - resolution = await this._getVideoResolution(file); + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; } catch (e) { - console.warn('Failed to get video resolution:', e); + console.warn('Failed to get video metadata:', e); } + + results.push({ + type, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + duration, + }, + }); + continue; } results.push({ @@ -427,6 +446,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), }, }); } @@ -473,6 +494,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: photo.size, + creationDate: new Date().toISOString(), }, }); }; @@ -494,7 +517,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getVideoResolution(videoFile: File): Promise { + private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; @@ -503,13 +526,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { // Clean up URL.revokeObjectURL(video.src); const resolution = `${video.videoWidth}x${video.videoHeight}`; - resolve(resolution); + const duration = video.duration; + resolve({ resolution, duration }); }; video.onerror = () => { - // Clean up and return default + // Clean up and return defaults URL.revokeObjectURL(video.src); - resolve('0x0'); + resolve({ resolution: '0x0', duration: 0 }); }; video.src = URL.createObjectURL(videoFile); From ebdf7ae9f8dd6a8e7a4d482d0c63c55b5b84f27a Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 09:15:02 -0400 Subject: [PATCH 06/29] fix: remove limit parameter from Web --- README.md | 2 +- src/definitions.ts | 2 +- src/web.ts | 20 +++++++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ae19024..fdb35a8 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ Allows the user to pick multiple pictures from the photo gallery. | ---------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----- | | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | -| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. | 0 | 8.1.0 | +| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | | **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 6acd866..09a267a 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -282,7 +282,7 @@ export interface ChooseFromGalleryOptions { * The maximum number of media files that the user can choose. * Only applicable if `allowMultipleSelection` is `true`. * Any non-positive number will be treated as unlimited. - * Note: This option is only supported on Android 13+ and iOS. + * Note: This option is only supported on Android 13+ and iOS. Not available on Web. * @default 0 * * @since 8.1.0 diff --git a/src/web.ts b/src/web.ts index 70b98ca..c8f6508 100644 --- a/src/web.ts +++ b/src/web.ts @@ -326,7 +326,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.hidden = true; document.body.appendChild(input); input.addEventListener('change', async (_e: any) => { - const file = input.files![0]; + if (!input.files || input.files.length === 0) { + reject(new CapacitorException('No file selected')); + cleanup(); + return; + } + + const file = input.files[0]; let format = 'jpeg'; if (file.type === 'image/png') { @@ -390,13 +396,17 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.multiple = options.allowMultipleSelection ?? false; document.body.appendChild(input); input.addEventListener('change', async (_e: any) => { + if (!input.files || input.files.length === 0) { + reject(new CapacitorException('No files selected')); + cleanup(); + return; + } + const results: MediaResult[] = []; - const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; - const filesToProcess = Math.min(limit, input.files!.length); // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < filesToProcess; i++) { - const file = input.files![i]; + for (let i = 0; i < input.files.length; i++) { + const file = input.files[i]; let format = 'jpeg'; let type = MediaType.Photo; let resolution = '0x0'; From 8db1dbd45e17084994f0d9826e9781b1dfe7b7d6 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 09:55:44 -0400 Subject: [PATCH 07/29] fix: add thumbnail to chooseFromGallery, for both images and videos --- README.md | 16 ++++---- src/definitions.ts | 1 + src/web.ts | 98 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index fdb35a8..42ef57e 100644 --- a/README.md +++ b/README.md @@ -349,14 +349,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaResult -| Prop | Type | Description | Since | -| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | -| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | -| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. | 8.1.0 | -| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | -| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | +| Prop | Type | Description | Since | +| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | +| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | +| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. | 8.1.0 | +| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | +| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | #### MediaMetadata diff --git a/src/definitions.ts b/src/definitions.ts index 09a267a..fddd998 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -446,6 +446,7 @@ export interface MediaResult { /** * Returns the thumbnail of the media, base64 encoded. * On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. + * On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. * * @since 8.1.0 */ diff --git a/src/web.ts b/src/web.ts index c8f6508..2f17ef2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -420,22 +420,41 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { // Get resolution from image file resolution = await this._getImageResolution(file); + + // Get base64 thumbnail for image + const thumbnail = await this._getBase64FromFile(file); + + results.push({ + type, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + }, + }); } else if (file.type.startsWith('video/')) { type = MediaType.Video; format = file.type.split('/')[1]; - // Get resolution and duration from video file + // Get resolution, duration, and thumbnail from video file let duration: number | undefined; + let thumbnail: string | undefined; try { const videoMetadata = await this._getVideoMetadata(file); resolution = videoMetadata.resolution; duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; } catch (e) { console.warn('Failed to get video metadata:', e); } results.push({ type, + thumbnail, webPath: URL.createObjectURL(file), saved: false, metadata: { @@ -446,20 +465,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { duration, }, }); - continue; } - - results.push({ - type, - webPath: URL.createObjectURL(file), - saved: false, - metadata: { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }, - }); } resolve({ results }); cleanup(); @@ -527,17 +533,69 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number }> { + private _getBase64FromFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const dataUrl = reader.result as string; + const base64 = dataUrl.split(',')[1]; + resolve(base64); + }; + reader.onerror = (e) => { + reject(e); + }; + reader.readAsDataURL(file); + }); + } + + private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number; thumbnail?: string }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; + video.muted = true; video.onloadedmetadata = () => { - // Clean up - URL.revokeObjectURL(video.src); - const resolution = `${video.videoWidth}x${video.videoHeight}`; - const duration = video.duration; - resolve({ resolution, duration }); + // Seek to 1 second or 10% of duration to capture thumbnail + const seekTime = Math.min(1, video.duration * 0.1); + video.currentTime = seekTime; + }; + + video.onseeked = () => { + try { + // Create canvas and capture frame + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; + + // Clean up + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + thumbnail, + }); + } else { + // Clean up and return without thumbnail + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }); + } + } catch (e) { + console.warn('Failed to generate video thumbnail:', e); + // Clean up and return without thumbnail + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }); + } }; video.onerror = () => { From 99f89c56db9a3aba0985698399e19b8c7683dfbe Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 10:22:00 -0400 Subject: [PATCH 08/29] refactor: remove duplicated code --- src/web.ts | 239 ++++++++++++++++++++++++++--------------------------- 1 file changed, 117 insertions(+), 122 deletions(-) diff --git a/src/web.ts b/src/web.ts index 2f17ef2..7f3df78 100644 --- a/src/web.ts +++ b/src/web.ts @@ -93,37 +93,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private async cameraExperience(options: ImageOptions, resolve: any, reject: any) { - if (customElements.get('pwa-camera-modal')) { - const cameraModal: any = document.createElement('pwa-camera-modal'); - cameraModal.facingMode = options.direction === CameraDirection.Front ? 'user' : 'environment'; - document.body.appendChild(cameraModal); - try { - await cameraModal.componentOnReady(); - cameraModal.addEventListener('onPhoto', async (e: any) => { - const photo = e.detail; - - if (photo === null) { - reject(new CapacitorException('User cancelled photos app')); - } else if (photo instanceof Error) { - reject(photo); - } else { - resolve(await this._getCameraPhoto(photo, options)); - } - - cameraModal.dismiss(); - document.body.removeChild(cameraModal); - }); - - cameraModal.present(); - } catch (e) { - this.fileInputExperience(options, resolve, reject); - } - } else { - console.error( - `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, - ); - this.fileInputExperience(options, resolve, reject); - } + await this._setupPWACameraModal( + options.direction, + (photo) => this._getCameraPhoto(photo, options), + () => this.fileInputExperience(options, resolve, reject), + resolve, + reject + ); } private fileInputExperience(options: ImageOptions, resolve: any, reject: any) { @@ -246,11 +222,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private _getCameraPhoto(photo: Blob, options: ImageOptions) { return new Promise((resolve, reject) => { const reader = new FileReader(); - const format = photo.type.split('/')[1]; + const format = this._getFileFormat(photo); if (options.resultType === 'uri') { resolve({ webPath: URL.createObjectURL(photo), - format: format, + format, saved: false, }); } else { @@ -260,13 +236,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { if (options.resultType === 'dataUrl') { resolve({ dataUrl: r, - format: format, + format, saved: false, }); } else { resolve({ base64String: r.split(',')[1], - format: format, + format, saved: false, }); } @@ -279,69 +255,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { - if (customElements.get('pwa-camera-modal')) { - const cameraModal: any = document.createElement('pwa-camera-modal'); - cameraModal.facingMode = options.cameraDirection === CameraDirection.Front ? 'user' : 'environment'; - document.body.appendChild(cameraModal); - try { - await cameraModal.componentOnReady(); - cameraModal.addEventListener('onPhoto', async (e: any) => { - const photo = e.detail; - - if (photo === null) { - reject(new CapacitorException('User cancelled photos app')); - } else if (photo instanceof Error) { - reject(photo); - } else { - resolve(await this._getCameraPhotoAsMediaResult(photo)); - } - - cameraModal.dismiss(); - document.body.removeChild(cameraModal); - }); - - cameraModal.present(); - } catch (e) { - this.takePhotoCameraInputExperience(options, resolve, reject); - } - } else { - console.error( - `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, - ); - this.takePhotoCameraInputExperience(options, resolve, reject); - } + await this._setupPWACameraModal( + options.cameraDirection, + (photo) => this._getCameraPhotoAsMediaResult(photo), + () => this.takePhotoCameraInputExperience(options, resolve, reject), + resolve, + reject + ); } private takePhotoCameraInputExperience(options: TakePhotoOptions, resolve: any, reject: any) { - let input = document.querySelector('#_capacitor-camera-input-takephoto') as HTMLInputElement; + const input = this._createFileInput('_capacitor-camera-input-takephoto'); const cleanup = () => { input.parentNode?.removeChild(input); }; - if (!input) { - input = document.createElement('input') as HTMLInputElement; - input.id = '_capacitor-camera-input-takephoto'; - input.type = 'file'; - input.hidden = true; - document.body.appendChild(input); + if (!input.onchange) { input.addEventListener('change', async (_e: any) => { - if (!input.files || input.files.length === 0) { - reject(new CapacitorException('No file selected')); - cleanup(); + if (!this._validateFileInput(input, reject, cleanup)) { return; } - const file = input.files[0]; - let format = 'jpeg'; - - if (file.type === 'image/png') { - format = 'png'; - } else if (file.type === 'image/gif') { - format = 'gif'; - } - - // Get resolution from image file + const file = input.files![0]; + const format = this._getFileFormat(file); const resolution = await this._getImageResolution(file); const reader = new FileReader(); @@ -364,10 +301,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { reader.readAsDataURL(file); }); - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); + this._setupInputCancelListener(input, reject, cleanup); } input.accept = 'image/*'; @@ -382,46 +316,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private galleryInputExperience(options: ChooseFromGalleryOptions, resolve: any, reject: any) { - let input = document.querySelector('#_capacitor-camera-input-gallery') as HTMLInputElement; + const input = this._createFileInput('_capacitor-camera-input-gallery'); + input.multiple = options.allowMultipleSelection ?? false; const cleanup = () => { input.parentNode?.removeChild(input); }; - if (!input) { - input = document.createElement('input') as HTMLInputElement; - input.id = '_capacitor-camera-input-gallery'; - input.type = 'file'; - input.hidden = true; - input.multiple = options.allowMultipleSelection ?? false; - document.body.appendChild(input); + if (!input.onchange) { input.addEventListener('change', async (_e: any) => { - if (!input.files || input.files.length === 0) { - reject(new CapacitorException('No files selected')); - cleanup(); + if (!this._validateFileInput(input, reject, cleanup)) { return; } const results: MediaResult[] = []; // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < input.files.length; i++) { - const file = input.files[i]; - let format = 'jpeg'; + for (let i = 0; i < input.files!.length; i++) { + const file = input.files![i]; + const format = this._getFileFormat(file); let type = MediaType.Photo; let resolution = '0x0'; if (file.type.startsWith('image/')) { - if (file.type === 'image/png') { - format = 'png'; - } else if (file.type === 'image/gif') { - format = 'gif'; - } - - // Get resolution from image file resolution = await this._getImageResolution(file); - - // Get base64 thumbnail for image const thumbnail = await this._getBase64FromFile(file); results.push({ @@ -438,9 +356,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } else if (file.type.startsWith('video/')) { type = MediaType.Video; - format = file.type.split('/')[1]; - // Get resolution, duration, and thumbnail from video file let duration: number | undefined; let thumbnail: string | undefined; try { @@ -470,10 +386,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ results }); cleanup(); }); - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); + this._setupInputCancelListener(input, reject, cleanup); } // Set accept attribute based on mediaType @@ -493,9 +406,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { return new Promise(async (resolve, reject) => { const reader = new FileReader(); - const format = photo.type.split('/')[1]; - - // Get resolution from image blob + const format = this._getFileFormat(photo); const resolution = await this._getImageResolution(photo); reader.readAsDataURL(photo); @@ -521,6 +432,90 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private _getFileFormat(file: File | Blob): string { + if (file.type === 'image/png') { + return 'png'; + } else if (file.type === 'image/gif') { + return 'gif'; + } else if (file.type.startsWith('video/')) { + return file.type.split('/')[1]; + } else if (file.type.startsWith('image/')) { + return 'jpeg'; + } + return file.type.split('/')[1] || 'jpeg'; + } + + private _validateFileInput(input: HTMLInputElement, reject: any, cleanup: () => void): boolean { + if (!input.files || input.files.length === 0) { + const message = input.multiple ? 'No files selected' : 'No file selected'; + reject(new CapacitorException(message)); + cleanup(); + return false; + } + return true; + } + + private async _setupPWACameraModal( + cameraDirection: CameraDirection | undefined, + onPhotoCallback: (photo: Blob) => Promise, + fallbackCallback: () => void, + resolve: any, + reject: any + ): Promise { + if (customElements.get('pwa-camera-modal')) { + const cameraModal: any = document.createElement('pwa-camera-modal'); + cameraModal.facingMode = cameraDirection === CameraDirection.Front ? 'user' : 'environment'; + document.body.appendChild(cameraModal); + try { + await cameraModal.componentOnReady(); + cameraModal.addEventListener('onPhoto', async (e: any) => { + const photo = e.detail; + + if (photo === null) { + reject(new CapacitorException('User cancelled photos app')); + } else if (photo instanceof Error) { + reject(photo); + } else { + resolve(await onPhotoCallback(photo)); + } + + cameraModal.dismiss(); + document.body.removeChild(cameraModal); + }); + + cameraModal.present(); + } catch (e) { + fallbackCallback(); + } + } else { + console.error( + `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, + ); + fallbackCallback(); + } + } + + private _createFileInput(id: string): HTMLInputElement { + let input = document.querySelector(`#${id}`) as HTMLInputElement; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = id; + input.type = 'file'; + input.hidden = true; + document.body.appendChild(input); + } + + return input; + } + + private _setupInputCancelListener(input: HTMLInputElement, reject: any, cleanup: () => void): void { + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); From 5463939be1e01ff356520940d23a5b7dfe366a18 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 10:33:33 -0400 Subject: [PATCH 09/29] feat: use includeMetadata for takePhoto and chooseFromGallery on Web --- README.md | 14 ++++---- src/definitions.ts | 3 -- src/web.ts | 89 +++++++++++++++++++++++++++++----------------- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 42ef57e..beefa52 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | | **`presentationStyle`** | 'fullscreen' \| 'popover' | iOS only: The presentation style of the Camera. | 'fullscreen' | 8.1.0 | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/web/pwa-elements | | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | #### RecordVideoOptions @@ -419,7 +419,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | | **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | | **`presentationStyle`** | 'fullscreen' \| 'popover' | iOS only: The presentation style of media picker. | 'fullscreen' | 8.1.0 | @@ -446,11 +446,11 @@ Allows the user to pick multiple pictures from the photo gallery. #### EditURIPhotoOptions -| Prop | Type | Description | Default | Since | -| --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | -| **`uri`** | string | The URI that contains the photo to edit. | | 8.1.0 | -| **`saveToGallery`** | boolean | Whether to save the edited photo to the gallery. | false | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| Prop | Type | Description | Default | Since | +| --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`uri`** | string | The URI that contains the photo to edit. | | 8.1.0 | +| **`saveToGallery`** | boolean | Whether to save the edited photo to the gallery. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | #### GalleryPhotos diff --git a/src/definitions.ts b/src/definitions.ts index fddd998..b335c10 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -214,7 +214,6 @@ export interface TakePhotoOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 @@ -292,7 +291,6 @@ export interface ChooseFromGalleryOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 @@ -401,7 +399,6 @@ export interface EditURIPhotoOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 diff --git a/src/web.ts b/src/web.ts index 7f3df78..f2b7279 100644 --- a/src/web.ts +++ b/src/web.ts @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { await this._setupPWACameraModal( options.cameraDirection, - (photo) => this._getCameraPhotoAsMediaResult(photo), + (photo) => this._getCameraPhotoAsMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, reject @@ -279,23 +279,29 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![0]; const format = this._getFileFormat(file); - const resolution = await this._getImageResolution(file); const reader = new FileReader(); - reader.addEventListener('load', () => { + reader.addEventListener('load', async () => { const b64 = (reader.result as string).split(',')[1]; - resolve({ + + const result: MediaResult = { type: MediaType.Photo, thumbnail: b64, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), - }, - } as MediaResult); + }; + } + + resolve(result); cleanup(); }); @@ -336,51 +342,64 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![i]; const format = this._getFileFormat(file); let type = MediaType.Photo; - let resolution = '0x0'; if (file.type.startsWith('image/')) { - resolution = await this._getImageResolution(file); const thumbnail = await this._getBase64FromFile(file); - results.push({ + const result: MediaResult = { type, thumbnail, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), - }, - }); + }; + } + + results.push(result); } else if (file.type.startsWith('video/')) { type = MediaType.Video; - let duration: number | undefined; let thumbnail: string | undefined; - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); + let resolution: string | undefined; + let duration: number | undefined; + + if (options.includeMetadata) { + try { + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; + } catch (e) { + console.warn('Failed to get video metadata:', e); + } } - results.push({ + const result: MediaResult = { type, thumbnail, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata && resolution) { + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), duration, - }, - }); + }; + } + + results.push(result); } } resolve({ results }); @@ -403,28 +422,34 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { + private async _getCameraPhotoAsMediaResult(photo: Blob, includeMetadata: boolean): Promise { return new Promise(async (resolve, reject) => { const reader = new FileReader(); const format = this._getFileFormat(photo); - const resolution = await this._getImageResolution(photo); reader.readAsDataURL(photo); - reader.onloadend = () => { + reader.onloadend = async () => { const r = reader.result as string; const b64 = r.split(',')[1]; - resolve({ + + const result: MediaResult = { type: MediaType.Photo, thumbnail: b64, webPath: URL.createObjectURL(photo), saved: false, - metadata: { + }; + + if (includeMetadata) { + const resolution = await this._getImageResolution(photo); + result.metadata = { format, resolution, size: photo.size, creationDate: new Date().toISOString(), - }, - }); + }; + } + + resolve(result); }; reader.onerror = (e) => { reject(e); From db31e98c38ae56c40d080e10b5ec1b7a7c282819 Mon Sep 17 00:00:00 2001 From: Rui Mendes Date: Tue, 24 Mar 2026 16:09:01 +0000 Subject: [PATCH 10/29] fix: show photos on older methods when resultType base64 or dataUrl --- .../camera/old-methods/GetPhotoConfigurable.tsx | 4 ++++ .../src/components/camera/old-methods/types.ts | 2 ++ example-app/src/pages/TakePicturePage.tsx | 12 ++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx b/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx index 70ec169..3665f08 100644 --- a/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx +++ b/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx @@ -72,6 +72,8 @@ class GetPhotoConfigurable extends React.Component< this.props.onPhotoResult({ path: photo.path, webPath: photo.webPath, + base64String: photo.base64String, + dataUrl: photo.dataUrl, exif: photo.exif, }); } catch (e) { @@ -105,6 +107,8 @@ class GetPhotoConfigurable extends React.Component< this.props.onPhotoResult({ path: photo.path, webPath: photo.webPath, + base64String: photo.base64String, + dataUrl: photo.dataUrl, exif: photo.exif, }); } catch (e) { diff --git a/example-app/src/components/camera/old-methods/types.ts b/example-app/src/components/camera/old-methods/types.ts index cddd3b0..d77251d 100644 --- a/example-app/src/components/camera/old-methods/types.ts +++ b/example-app/src/components/camera/old-methods/types.ts @@ -30,5 +30,7 @@ export interface PickImagesConfig { export interface PhotoResult { path?: string; webPath?: string; + base64String?: string; + dataUrl?: string; exif?: any; } diff --git a/example-app/src/pages/TakePicturePage.tsx b/example-app/src/pages/TakePicturePage.tsx index 6411e55..cddaa21 100644 --- a/example-app/src/pages/TakePicturePage.tsx +++ b/example-app/src/pages/TakePicturePage.tsx @@ -58,14 +58,22 @@ class TakePicturePage extends React.Component<{}, ITakePicturePageState> { handlePhotoResult = (result: { path?: string; webPath?: string; + base64String?: string; + dataUrl?: string; exif?: any; }): void => { + const filePath = + result.path ?? + result.webPath ?? + result.dataUrl ?? + (result.base64String ? `data:image/jpeg;base64,${result.base64String}` : null); + this.setState({ - filePath: result.path ?? result.webPath ?? null, + filePath, metadata: JSON.stringify(result.exif, null, 2), }); - if (result.path || result.webPath) { + if (filePath) { MediaHistoryService.addMedia({ mediaType: "photo", method: "getPhoto", From 8b0f1c5f30ffeb4568b1101e374b466532a1ed0d Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 15:25:39 -0400 Subject: [PATCH 11/29] refactor: avoid code duplication and improve event listeners --- src/web.ts | 234 ++++++++++++++++++++--------------------------------- 1 file changed, 86 insertions(+), 148 deletions(-) diff --git a/src/web.ts b/src/web.ts index f2b7279..9eb95b9 100644 --- a/src/web.ts +++ b/src/web.ts @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { await this._setupPWACameraModal( options.cameraDirection, - (photo) => this._getCameraPhotoAsMediaResult(photo, options.includeMetadata ?? false), + (photo) => this._buildPhotoMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, reject @@ -271,44 +271,20 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.parentNode?.removeChild(input); }; - if (!input.onchange) { - input.addEventListener('change', async (_e: any) => { - if (!this._validateFileInput(input, reject, cleanup)) { - return; - } - - const file = input.files![0]; - const format = this._getFileFormat(file); - - const reader = new FileReader(); - reader.addEventListener('load', async () => { - const b64 = (reader.result as string).split(',')[1]; - - const result: MediaResult = { - type: MediaType.Photo, - thumbnail: b64, - webPath: URL.createObjectURL(file), - saved: false, - }; - - if (options.includeMetadata) { - const resolution = await this._getImageResolution(file); - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }; - } + input.onchange = async (_e: any) => { + if (!this._validateFileInput(input, reject, cleanup)) { + return; + } - resolve(result); - cleanup(); - }); + const file = input.files![0]; + resolve(await this._buildPhotoMediaResult(file, options.includeMetadata ?? false)); + cleanup(); + }; - reader.readAsDataURL(file); - }); - this._setupInputCancelListener(input, reject, cleanup); - } + input.oncancel = () => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }; input.accept = 'image/*'; if (options.cameraDirection === CameraDirection.Front) { @@ -329,84 +305,64 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.parentNode?.removeChild(input); }; - if (!input.onchange) { - input.addEventListener('change', async (_e: any) => { - if (!this._validateFileInput(input, reject, cleanup)) { - return; - } + input.onchange = async (_e: any) => { + if (!this._validateFileInput(input, reject, cleanup)) { + return; + } - const results: MediaResult[] = []; + const results: MediaResult[] = []; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < input.files!.length; i++) { - const file = input.files![i]; - const format = this._getFileFormat(file); - let type = MediaType.Photo; - - if (file.type.startsWith('image/')) { - const thumbnail = await this._getBase64FromFile(file); + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < input.files!.length; i++) { + const file = input.files![i]; - const result: MediaResult = { - type, - thumbnail, - webPath: URL.createObjectURL(file), - saved: false, - }; + if (file.type.startsWith('image/')) { + results.push(await this._buildPhotoMediaResult(file, options.includeMetadata ?? false)); + } else if (file.type.startsWith('video/')) { + const format = this._getFileFormat(file); + let thumbnail: string | undefined; + let resolution: string | undefined; + let duration: number | undefined; - if (options.includeMetadata) { - const resolution = await this._getImageResolution(file); - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }; + if (options.includeMetadata) { + try { + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; + } catch (e) { + console.warn('Failed to get video metadata:', e); } + } - results.push(result); - } else if (file.type.startsWith('video/')) { - type = MediaType.Video; - - let thumbnail: string | undefined; - let resolution: string | undefined; - let duration: number | undefined; - - if (options.includeMetadata) { - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); - } - } + const result: MediaResult = { + type: MediaType.Video, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + }; - const result: MediaResult = { - type, - thumbnail, - webPath: URL.createObjectURL(file), - saved: false, + if (options.includeMetadata && resolution) { + result.metadata = { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + duration, }; - - if (options.includeMetadata && resolution) { - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - duration, - }; - } - - results.push(result); } + + results.push(result); } - resolve({ results }); - cleanup(); - }); - this._setupInputCancelListener(input, reject, cleanup); - } + } + resolve({ results }); + cleanup(); + }; + + input.oncancel = () => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }; // Set accept attribute based on mediaType const mediaType = options.mediaType ?? MediaTypeSelection.Photo; @@ -422,41 +378,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private async _getCameraPhotoAsMediaResult(photo: Blob, includeMetadata: boolean): Promise { - return new Promise(async (resolve, reject) => { - const reader = new FileReader(); - const format = this._getFileFormat(photo); - - reader.readAsDataURL(photo); - reader.onloadend = async () => { - const r = reader.result as string; - const b64 = r.split(',')[1]; - - const result: MediaResult = { - type: MediaType.Photo, - thumbnail: b64, - webPath: URL.createObjectURL(photo), - saved: false, - }; - - if (includeMetadata) { - const resolution = await this._getImageResolution(photo); - result.metadata = { - format, - resolution, - size: photo.size, - creationDate: new Date().toISOString(), - }; - } - - resolve(result); - }; - reader.onerror = (e) => { - reject(e); - }; - }); - } - private _getFileFormat(file: File | Blob): string { if (file.type === 'image/png') { return 'png'; @@ -470,6 +391,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return file.type.split('/')[1] || 'jpeg'; } + private async _buildPhotoMediaResult(file: File | Blob, includeMetadata: boolean): Promise { + const format = this._getFileFormat(file); + const thumbnail = await this._getBase64FromFile(file); + + const result: MediaResult = { + type: MediaType.Photo, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + }; + + if (includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { + format, + resolution, + size: file.size, + creationDate: 'lastModified' in file ? new Date(file.lastModified).toISOString() : new Date().toISOString(), + }; + } + + return result; + } + private _validateFileInput(input: HTMLInputElement, reject: any, cleanup: () => void): boolean { if (!input.files || input.files.length === 0) { const message = input.multiple ? 'No files selected' : 'No file selected'; @@ -534,13 +479,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return input; } - private _setupInputCancelListener(input: HTMLInputElement, reject: any, cleanup: () => void): void { - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); - } - private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); @@ -553,7 +491,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getBase64FromFile(file: File): Promise { + private _getBase64FromFile(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { From 0f4c9f3e2678312b432db01bf712162ce6aea7dd Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 17:39:42 -0400 Subject: [PATCH 12/29] chore(android): update Android bridge with new lib version --- .../main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt index 8bcc33e..c46305f 100644 --- a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt +++ b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt @@ -832,9 +832,6 @@ class IonCameraFlow( activity, intent, ionParams, - { image -> - //TODO remove this callback - }, { mediaResult -> handleMediaResult(mediaResult) }, @@ -938,7 +935,6 @@ class IonCameraFlow( correctOrientation = correctOrientation, saveToPhotoAlbum = saveToGallery, includeMetadata = includeMetadata, - latestVersion = true //TODO check this, because now we don't have resultType in the new Api ) } From d0d2a6f88291f4a736f7602cc5bbba7f47e46166 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 08:57:14 -0400 Subject: [PATCH 13/29] chore: remove redundant doc --- src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index b335c10..30e02ad 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -281,7 +281,7 @@ export interface ChooseFromGalleryOptions { * The maximum number of media files that the user can choose. * Only applicable if `allowMultipleSelection` is `true`. * Any non-positive number will be treated as unlimited. - * Note: This option is only supported on Android 13+ and iOS. Not available on Web. + * Note: This option is only supported on Android 13+ and iOS. * @default 0 * * @since 8.1.0 From 6573fecb1dcdd2ed60ddc6ee99fd026d86cdc244 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:15:58 -0400 Subject: [PATCH 14/29] chore: update docs --- README.md | 18 +++++++++--------- src/definitions.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index beefa52..83d2982 100644 --- a/README.md +++ b/README.md @@ -349,14 +349,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaResult -| Prop | Type | Description | Since | -| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | -| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | -| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. | 8.1.0 | -| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | -| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | +| Prop | Type | Description | Since | +| --------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | +| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | +| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a full-resolution JPEG frame captured from the video is returned, base64 encoded at 80% quality. | 8.1.0 | +| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | +| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | #### MediaMetadata @@ -418,7 +418,7 @@ Allows the user to pick multiple pictures from the photo gallery. | ---------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----- | | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | -| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | +| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. | 0 | 8.1.0 | | **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 30e02ad..1b4b946 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -443,7 +443,7 @@ export interface MediaResult { /** * Returns the thumbnail of the media, base64 encoded. * On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. - * On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. + * On Web, for `MediaType.Video`, a full-resolution JPEG frame captured from the video is returned, base64 encoded at 80% quality. * * @since 8.1.0 */ From 2ca861734553db0f14ce69194315be4e3bc26524 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:17:31 -0400 Subject: [PATCH 15/29] chore: update docs --- README.md | 2 +- src/definitions.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83d2982..b3a9bee 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | | **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | | **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | | **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 1b4b946..e420b2c 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -511,6 +511,7 @@ export interface MediaMetadata { /** * The date and time the media was created, in ISO 8601 format. * If creation date is not available (e.g. Android 7 and below), the last modified date is returned. + * For Web, the last modified date is always returned. * * @since 8.1.0 */ From aeaadff02d7245c0c96c31bb2cf9c138316885be Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Wed, 25 Mar 2026 09:18:37 -0400 Subject: [PATCH 16/29] chore: update src/definitions.ts Co-authored-by: Pedro Bilro --- src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index e420b2c..e6524e2 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -495,7 +495,7 @@ export interface MediaMetadata { * The format of the image, ex: jpeg, png, mp4. * * Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. - * gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. + * gif is only supported for `chooseFromGallery` on Web. * * @since 8.1.0 */ From 8998b2c5ef02b6771e72a5ba8d051e014a792559 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:28:10 -0400 Subject: [PATCH 17/29] refactor: make Photo.saved optional like other parameters (e.g. exif) --- README.md | 16 ++++++++-------- src/definitions.ts | 2 +- src/web.ts | 3 --- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b3a9bee..b3a4c1f 100644 --- a/README.md +++ b/README.md @@ -361,14 +361,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaMetadata -| Prop | Type | Description | Since | -| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`size`** | number | File size of the media, in bytes. | 8.1.0 | -| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | -| **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | -| **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | -| **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | +| Prop | Type | Description | Since | +| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| **`size`** | number | File size of the media, in bytes. | 8.1.0 | +| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | +| **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery` on Web. | 8.1.0 | +| **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | +| **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | #### TakePhotoOptions diff --git a/src/definitions.ts b/src/definitions.ts index e6524e2..4f66437 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -722,7 +722,7 @@ export interface Photo { * * @since 1.1.0 */ - saved: boolean; + saved?: boolean; } export interface GalleryPhotos { diff --git a/src/web.ts b/src/web.ts index 9eb95b9..c7a25b2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -227,7 +227,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ webPath: URL.createObjectURL(photo), format, - saved: false, }); } else { reader.readAsDataURL(photo); @@ -237,13 +236,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ dataUrl: r, format, - saved: false, }); } else { resolve({ base64String: r.split(',')[1], format, - saved: false, }); } }; From 8e3f565695b96756bcb433e6f069846961494f68 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:43:36 -0400 Subject: [PATCH 18/29] refactor: make MediaMetadata.resolution optional --- src/definitions.ts | 2 +- src/web.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/definitions.ts b/src/definitions.ts index 4f66437..c3b189b 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -506,7 +506,7 @@ export interface MediaMetadata { * * @since 8.1.0 */ - resolution: string; + resolution?: string; /** * The date and time the media was created, in ISO 8601 format. diff --git a/src/web.ts b/src/web.ts index c7a25b2..929a48b 100644 --- a/src/web.ts +++ b/src/web.ts @@ -503,7 +503,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } - private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number; thumbnail?: string }> { + private _getVideoMetadata(videoFile: File): Promise<{ resolution?: string; duration?: number; thumbnail?: string }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; @@ -556,7 +556,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { video.onerror = () => { // Clean up and return defaults URL.revokeObjectURL(video.src); - resolve({ resolution: '0x0', duration: 0 }); + resolve({}); }; video.src = URL.createObjectURL(videoFile); From 92ede100c2967f19e42d88e439062c9ea36e6215 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:57:38 -0400 Subject: [PATCH 19/29] refactor: remove duplicated code --- src/web.ts | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/web.ts b/src/web.ts index 929a48b..32b014d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -516,8 +516,12 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }; video.onseeked = () => { + const result: { resolution?: string; duration?: number; thumbnail?: string } = { + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }; + try { - // Create canvas and capture frame const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; @@ -525,32 +529,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { if (ctx) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; - - // Clean up - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - thumbnail, - }); - } else { - // Clean up and return without thumbnail - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - }); + result.thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; } } catch (e) { console.warn('Failed to generate video thumbnail:', e); - // Clean up and return without thumbnail - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - }); } + + URL.revokeObjectURL(video.src); + resolve(result); }; video.onerror = () => { From 6772ebc49c8583d507f134ec8bdb8d285b6e0e97 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:01:28 -0400 Subject: [PATCH 20/29] refactor: avoid returning 0 resolution --- src/web.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web.ts b/src/web.ts index 32b014d..588e814 100644 --- a/src/web.ts +++ b/src/web.ts @@ -476,7 +476,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return input; } - private async _getImageResolution(image: Blob | File): Promise { + private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); const resolution = `${bitmap.width}x${bitmap.height}`; @@ -484,7 +484,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return resolution; } catch (e) { console.warn('Failed to get image resolution:', e); - return '0x0'; + return undefined; } } From 2e37fc904b6db974705886d5150204c6ba05b11e Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:09:09 -0400 Subject: [PATCH 21/29] fix: always return video thumbnail --- src/web.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/web.ts b/src/web.ts index 588e814..d571a7e 100644 --- a/src/web.ts +++ b/src/web.ts @@ -321,15 +321,16 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { let resolution: string | undefined; let duration: number | undefined; - if (options.includeMetadata) { - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); + try { + const videoInfo = await this._getVideoMetadata(file); + thumbnail = videoInfo.thumbnail; + + if (options.includeMetadata) { + resolution = videoInfo.resolution; + duration = videoInfo.duration; } + } catch (e) { + console.warn('Failed to get video metadata:', e); } const result: MediaResult = { From 17525f6340c92fcc7e9d1450fa322e3957e8d267 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:15:04 -0400 Subject: [PATCH 22/29] fix: remove resolution from if condition --- src/web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web.ts b/src/web.ts index d571a7e..c7ac9e2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -340,7 +340,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, }; - if (options.includeMetadata && resolution) { + if (options.includeMetadata) { result.metadata = { format, resolution, From 02bd832dd3168cb55fe91e87c9ce86bfe22178ad Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:34:33 -0400 Subject: [PATCH 23/29] chore: run prettier to fix lint issues --- src/web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web.ts b/src/web.ts index c7ac9e2..62ca1ec 100644 --- a/src/web.ts +++ b/src/web.ts @@ -98,7 +98,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { (photo) => this._getCameraPhoto(photo, options), () => this.fileInputExperience(options, resolve, reject), resolve, - reject + reject, ); } @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { (photo) => this._buildPhotoMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, - reject + reject, ); } @@ -428,7 +428,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { onPhotoCallback: (photo: Blob) => Promise, fallbackCallback: () => void, resolve: any, - reject: any + reject: any, ): Promise { if (customElements.get('pwa-camera-modal')) { const cameraModal: any = document.createElement('pwa-camera-modal'); From 404accefeed46f10a8cc24c87ae54c6d01b33a54 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 14:29:03 -0400 Subject: [PATCH 24/29] fix: revert previous commit and keep Photo API as it was --- src/definitions.ts | 2 +- src/web.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index c3b189b..ea5e461 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -722,7 +722,7 @@ export interface Photo { * * @since 1.1.0 */ - saved?: boolean; + saved: boolean; } export interface GalleryPhotos { diff --git a/src/web.ts b/src/web.ts index 62ca1ec..7b21f69 100644 --- a/src/web.ts +++ b/src/web.ts @@ -227,6 +227,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ webPath: URL.createObjectURL(photo), format, + saved: false, }); } else { reader.readAsDataURL(photo); @@ -236,11 +237,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ dataUrl: r, format, + saved: false, }); } else { resolve({ base64String: r.split(',')[1], format, + saved: false, }); } }; From c2e8dd1f6ce6445bf59d7b2c3048b9ddf64880b5 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 16:47:09 -0400 Subject: [PATCH 25/29] test: test commit --- ARCHITECTURE.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a94ce68 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,119 @@ +# Capacitor Camera Plugin Architecture + +> **Repository:** capacitor-camera (`@capacitor/camera`) +> **Runtime Environment:** Library/SDK -- runs in user's browser (Web), Android app process, or iOS app process depending on the host Capacitor application +> **Last Updated:** 2026-03-25 + +## Overview + +This is a Capacitor plugin that provides a cross-platform API for capturing photos, recording video, picking media from the device gallery, and editing images. The TypeScript interface layer defines a single `CameraPlugin` contract; platform-specific native implementations (Android/Kotlin+Java, iOS/Swift) and a Web fallback (TypeScript/DOM APIs) fulfil that contract at runtime via Capacitor's plugin bridge. + +## Architecture Diagram + +```mermaid +graph TB + Plugin["@capacitor/camera
Runs in: Host app (Browser / Android / iOS)"] + + CapBridge["Capacitor Bridge
EXTERNAL"] + OSCamera["OS Camera App
EXTERNAL"] + OSGallery["OS Photo/Video Picker
EXTERNAL"] + OSEditor["OS Image Editor
EXTERNAL"] + PhotoLibrary["Device Photo Library
EXTERNAL"] + FileSystem["Device File System / Cache
EXTERNAL"] + BrowserAPIs["Browser File & Media APIs
EXTERNAL"] + PWAElements["PWA Elements
EXTERNAL"] + IONCameraLib["IONCAMRLib (Android)
EXTERNAL"] + + Plugin -->|"Native bridge calls
Synchronous"| CapBridge + Plugin -->|"Intent / UIImagePickerController
Synchronous"| OSCamera + Plugin -->|"PickVisualMedia / PHPicker
Synchronous"| OSGallery + Plugin -->|"ACTION_EDIT Intent / In-App Crop
Synchronous"| OSEditor + Plugin -->|"MediaStore / PHPhotoLibrary
Synchronous"| PhotoLibrary + Plugin -->|"File I/O, FileProvider, temp files
Synchronous"| FileSystem + Plugin -->|"File input, FileReader, Canvas
Synchronous"| BrowserAPIs + Plugin -.->|"pwa-camera-modal (optional)
Synchronous"| PWAElements + Plugin -->|"Camera, Gallery, Video, Edit managers
Synchronous"| IONCameraLib + + classDef thisRepo fill:#e0f2f1,stroke:#00796b,stroke-width:3px + classDef external fill:#ffe1e1,stroke:#d32f2f,stroke-width:2px,stroke-dasharray: 5 5 + classDef database fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + + class Plugin thisRepo + class CapBridge,OSCamera,OSGallery,OSEditor,PWAElements,IONCameraLib external + class PhotoLibrary,FileSystem,BrowserAPIs database +``` + +## External Integrations + +| External Service | Platform | Communication Type | Purpose | +|---|---|---|---| +| Capacitor Bridge (`@capacitor/core`) | All | Sync (native bridge) | Routes JS calls to the correct platform implementation; manages plugin registration | +| OS Camera App | Android, iOS | Sync (Intent / UIImagePickerController) | Capture photos and record videos via the system camera | +| OS Photo/Video Picker | Android, iOS | Sync (PickVisualMedia / PHPickerViewController) | Select media from the device gallery | +| OS Image Editor | Android | Sync (ACTION_EDIT Intent) | External image editing when `editInApp` is `false`; falls back to in-app editor | +| IONCAMRLib | Android | Sync (library call) | Ionic's internal Android camera library providing `IONCAMRCameraManager`, `IONCAMRGalleryManager`, `IONCAMRVideoManager`, `IONCAMREditManager` for new-API flows | +| Device Photo Library (MediaStore / PHPhotoLibrary) | Android, iOS | Sync (ContentResolver / Photos framework) | Save images/videos to gallery; read limited-library photos on iOS | +| Device File System / Cache | Android, iOS | Sync (File I/O, FileProvider) | Store temporary image/video files; provide content URIs to other apps | +| Browser File & Media APIs | Web | Sync (DOM APIs) | ``, `FileReader`, `createImageBitmap`, `Canvas`, `URL.createObjectURL` for capturing/processing photos and video metadata | +| PWA Elements (`pwa-camera-modal`) | Web | Sync (Custom Element) | Optional camera UI for web; falls back to file input if not installed | +| AndroidX ExifInterface | Android | Sync (library call) | Read/write EXIF metadata from captured or picked images | + +## Architectural Tenets + +### T1. A single TypeScript interface must define the contract for all platforms + +Every public method available to consumers is declared in `definitions.ts` via the `CameraPlugin` interface. Android, iOS, and Web implementations must conform to this contract. This ensures that consumers write one set of calls regardless of the runtime platform, and that the public API surface is governed by a single source of truth. + +**Evidence:** +- `src/definitions.ts` (in `CameraPlugin` interface) -- declares all methods: `takePhoto`, `recordVideo`, `playVideo`, `chooseFromGallery`, `editPhoto`, `editURIPhoto`, `pickLimitedLibraryPhotos`, `getLimitedLibraryPhotos`, `checkPermissions`, `requestPermissions`, plus deprecated `getPhoto` and `pickImages` +- `src/web.ts` (in `CameraWeb` class) -- `implements CameraPlugin`, providing web-specific implementations or throwing `unimplemented` for unsupported methods +- `android/.../CameraPlugin.kt` -- every `@PluginMethod` function name matches a method in the TypeScript interface +- `ios/.../CameraPlugin.swift` (in `pluginMethods` array) -- registers method names matching the TypeScript definitions + +### T2. Platform-specific logic must not leak into the shared TypeScript layer + +The TypeScript layer (`src/`) contains only the interface definitions, enum types, and the `registerPlugin` call. It holds zero platform-detection logic, zero Android/iOS-specific code, and delegates entirely to Capacitor's runtime plugin resolution. This keeps the published npm package lightweight and ensures native concerns stay in their respective platform directories. + +**Evidence:** +- `src/index.ts` -- only calls `registerPlugin('Camera', { web: () => new CameraWeb() })`, no conditional imports or platform checks +- `src/definitions.ts` -- pure TypeScript types and enums; no runtime code +- `src/web.ts` -- implements the web fallback using only DOM/browser APIs; methods unsupported on web throw `this.unimplemented()` + +### T3. Android new-API flows must delegate to IONCAMRLib managers, not perform media operations directly + +The `IonCameraFlow` class (which handles all non-deprecated methods on Android) delegates camera capture, video recording, gallery selection, and image editing to dedicated manager classes from `io.ionic.libs.ioncameralib` (`IONCAMRCameraManager`, `IONCAMRVideoManager`, `IONCAMRGalleryManager`, `IONCAMREditManager`). The plugin layer is responsible only for parsing `PluginCall` options into settings objects, wiring up Android `ActivityResultLauncher` callbacks, and formatting results back into `JSObject`. This separation keeps the plugin thin and allows the underlying media library to be tested and evolved independently. + +**Evidence:** +- `android/.../IonCameraFlow.kt` (in `load`) -- instantiates four separate manager objects from `io.ionic.libs.ioncameralib` +- `android/.../IonCameraFlow.kt` (in `openCamera`, `openRecordVideo`, `openGallery`) -- delegates to `cameraManager.takePhoto()`, `cameraManager.recordVideo()`, `galleryManager.chooseFromGallery()` respectively +- `android/.../IonCameraFlow.kt` (in `processResult`, `processResultFromVideo`, `processResultFromGallery`, `processResultFromEdit`) -- delegates processing to manager methods, only handling the JSObject serialization in callbacks + +### T4. Deprecated API paths must be isolated from new API paths + +The Android implementation maintains two completely separate flow classes: `LegacyCameraFlow` (Java) for the deprecated `getPhoto` and `pickImages` methods, and `IonCameraFlow` (Kotlin) for the new `takePhoto`, `recordVideo`, `chooseFromGallery`, `editPhoto`, and `editURIPhoto` methods. The plugin entry point (`CameraPlugin.kt`) routes each call to the appropriate flow. This isolation ensures that evolving the new API does not risk regressions in the deprecated API, and allows eventual removal of the legacy path without touching the new code. + +**Evidence:** +- `android/.../CameraPlugin.kt` (in `load`) -- instantiates both `legacyFlow` and `ionFlow` as separate objects +- `android/.../CameraPlugin.kt` (in `getPhoto`) -- routes to `legacyFlow.getPhoto(call)` +- `android/.../CameraPlugin.kt` (in `takePhoto`, `recordVideo`, `chooseFromGallery`, `editPhoto`, `editURIPhoto`) -- routes to corresponding `ionFlow` methods +- `src/definitions.ts` -- deprecated methods (`getPhoto`, `pickImages`) carry `@deprecated` JSDoc annotations directing consumers to the new methods + +### T5. Permission handling must be abstracted behind a shared helper, not duplicated per flow + +On Android, both `LegacyCameraFlow` and `IonCameraFlow` rely on a `PermissionHelper` class that wraps the Capacitor plugin's permission primitives (`isPermissionDeclared`, `getPermissionState`, `requestPermissionForAlias`, `requestPermissionForAliases`) as injectable functions. This avoids duplicating permission logic across flows and makes it possible to test permission behavior independently from the plugin lifecycle. + +**Evidence:** +- `android/.../PermissionHelper.kt` -- a standalone class accepting four function parameters that abstract all permission operations +- `android/.../CameraPlugin.kt` (in `load`) -- constructs a single `PermissionHelper` instance and passes it to both `legacyFlow` and `ionFlow` +- `android/.../IonCameraFlow.kt` (in `checkCameraPermissions`) -- uses `permissionHelper.isPermissionDeclared()`, `permissionHelper.getPermissionState()`, and `permissionHelper.requestPermissionForAlias()` +- `android/.../LegacyCameraFlow.java` (in `checkCameraPermissions`) -- uses the same `permissionHelper` methods + +### Current Phase Constraints + +**Deprecated API coexistence:** The `getPhoto` and `pickImages` methods and their supporting types (`ImageOptions`, `Photo`, `GalleryImageOptions`, `CameraSource`, `CameraResultType`) remain in the codebase alongside the new API (`takePhoto`, `chooseFromGallery`, etc.). Both paths are actively maintained. + +> **Expires when:** The deprecated methods are removed in a future major version, as noted in the `@deprecated` JSDoc annotations in `src/definitions.ts`. + +**iOS only implements the legacy API surface:** The iOS Swift implementation currently registers only `getPhoto`, `pickImages`, `checkPermissions`, `requestPermissions`, `pickLimitedLibraryPhotos`, and `getLimitedLibraryPhotos`. The newer methods (`takePhoto`, `recordVideo`, `playVideo`, `chooseFromGallery`, `editPhoto`, `editURIPhoto`) are not yet implemented on iOS. + +> **Expires when:** The iOS implementation is updated to support the full new API surface defined in `CameraPlugin` interface. From caeff57dbb878861a81546ecf65e3d729f407d73 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 26 Mar 2026 09:04:18 +0000 Subject: [PATCH 26/29] chore(example-app): Remove `webUseInput` for chooseFromGallery --- .../camera/ChooseFromGalleryConfigurable.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/example-app/src/components/camera/ChooseFromGalleryConfigurable.tsx b/example-app/src/components/camera/ChooseFromGalleryConfigurable.tsx index 7de3360..1b07a2e 100644 --- a/example-app/src/components/camera/ChooseFromGalleryConfigurable.tsx +++ b/example-app/src/components/camera/ChooseFromGalleryConfigurable.tsx @@ -85,7 +85,6 @@ class ChooseFromGalleryConfigurable extends React.Component< targetWidth: config.targetWidth, targetHeight: config.targetHeight, correctOrientation: config.correctOrientation, - webUseInput: config.webUseInput, }); console.log('chooseFromGallery result', result); @@ -249,16 +248,6 @@ class ChooseFromGalleryConfigurable extends React.Component< /> - - Web Use Input - - this.updateConfig("webUseInput", e.detail.checked) - } - /> - - Date: Thu, 26 Mar 2026 09:09:16 +0000 Subject: [PATCH 27/29] chore(example-app): Minor notice for MediaHistoryPage in PWA --- example-app/src/pages/MediaHistoryPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example-app/src/pages/MediaHistoryPage.tsx b/example-app/src/pages/MediaHistoryPage.tsx index b9b9d5b..02e9bb6 100644 --- a/example-app/src/pages/MediaHistoryPage.tsx +++ b/example-app/src/pages/MediaHistoryPage.tsx @@ -20,6 +20,7 @@ import { MediaHistoryItem, } from "../services/MediaHistoryService"; import { FileViewer } from "@capacitor/file-viewer"; +import { Capacitor } from "@capacitor/core"; const MediaHistoryPage: React.FC = () => { const [history, setHistory] = useState([]); @@ -35,6 +36,10 @@ const MediaHistoryPage: React.FC = () => { const openFile = async (item: MediaHistoryItem): Promise => { try { + if (Capacitor.getPlatform() === 'web') { + alert('Opening media from history is not available on Web/PWA.'); + return; + } const filePath = item.uri ?? item.path ?? ''; if (!filePath) { alert('No file path available for this item'); From 933a66aa7b1e6a94262d9c6c887a1bbf2ffdfdb5 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 26 Mar 2026 09:34:49 +0000 Subject: [PATCH 28/29] chore(example-app): Show thumbnail from plugin on video --- example-app/src/components/camera/MediaCarousel.tsx | 6 +++++- example-app/src/components/camera/VideoWithMetadata.tsx | 3 +++ example-app/src/pages/RecordVideoPage.tsx | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/example-app/src/components/camera/MediaCarousel.tsx b/example-app/src/components/camera/MediaCarousel.tsx index c99adee..a0ef286 100644 --- a/example-app/src/components/camera/MediaCarousel.tsx +++ b/example-app/src/components/camera/MediaCarousel.tsx @@ -66,7 +66,11 @@ const MediaCarousel: React.FC = ({ media, onEditPhoto }) => return ( {isVideo(item) ? ( - + ) : ( = ({ filePath, metadata, + thumbnail, }) => { const formatMetadata = (meta: MediaMetadata | string | null | undefined): string => { if (!meta) return ''; @@ -51,6 +53,7 @@ const VideoWithMetadata: React.FC = ({