Skip to content
Merged
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
69cdaf6
feat: add web implementation for takePhoto and chooseFromGallery
alexgerardojacinto Mar 23, 2026
fda773f
Merge branch 'feat/RMET-4099/camera-unification' into feat/RMET-5029/…
OS-pedrogustavobilro Mar 24, 2026
6f2504b
feat: get resolution for images and videos
alexgerardojacinto Mar 24, 2026
ce960fe
refactor: remove redundant assign
alexgerardojacinto Mar 24, 2026
96e5abf
fix: use rear camera as default
alexgerardojacinto Mar 24, 2026
a05e1df
feat: add size, creationDate, and duration to new Web functions
alexgerardojacinto Mar 24, 2026
ebdf7ae
fix: remove limit parameter from Web
alexgerardojacinto Mar 24, 2026
8db1dbd
fix: add thumbnail to chooseFromGallery, for both images and videos
alexgerardojacinto Mar 24, 2026
99f89c5
refactor: remove duplicated code
alexgerardojacinto Mar 24, 2026
5463939
feat: use includeMetadata for takePhoto and chooseFromGallery on Web
alexgerardojacinto Mar 24, 2026
db31e98
fix: show photos on older methods when resultType base64 or dataUrl
OS-ruimoreiramendes Mar 24, 2026
8b0f1c5
refactor: avoid code duplication and improve event listeners
alexgerardojacinto Mar 24, 2026
0f4c9f3
chore(android): update Android bridge with new lib version
alexgerardojacinto Mar 24, 2026
d0d2a6f
chore: remove redundant doc
alexgerardojacinto Mar 25, 2026
6573fec
chore: update docs
alexgerardojacinto Mar 25, 2026
2ca8617
chore: update docs
alexgerardojacinto Mar 25, 2026
aeaadff
chore: update src/definitions.ts
alexgerardojacinto Mar 25, 2026
8998b2c
refactor: make Photo.saved optional like other parameters (e.g. exif)
alexgerardojacinto Mar 25, 2026
8e3f565
refactor: make MediaMetadata.resolution optional
alexgerardojacinto Mar 25, 2026
92ede10
refactor: remove duplicated code
alexgerardojacinto Mar 25, 2026
6772ebc
refactor: avoid returning 0 resolution
alexgerardojacinto Mar 25, 2026
2e37fc9
fix: always return video thumbnail
alexgerardojacinto Mar 25, 2026
17525f6
fix: remove resolution from if condition
alexgerardojacinto Mar 25, 2026
02bd832
chore: run prettier to fix lint issues
alexgerardojacinto Mar 25, 2026
404acce
fix: revert previous commit and keep Photo API as it was
alexgerardojacinto Mar 25, 2026
c2e8dd1
test: test commit
alexgerardojacinto Mar 25, 2026
caeff57
chore(example-app): Remove `webUseInput` for chooseFromGallery
OS-pedrogustavobilro Mar 26, 2026
dac03f1
chore(example-app): Minor notice for MediaHistoryPage in PWA
OS-pedrogustavobilro Mar 26, 2026
933a66a
chore(example-app): Show thumbnail from plugin on video
OS-pedrogustavobilro Mar 26, 2026
09f2bd4
Revert "test: test commit"
alexgerardojacinto Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 203 additions & 5 deletions src/web.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,27 +20,37 @@
} from './definitions';

export class CameraWeb extends WebPlugin implements CameraPlugin {
async takePhoto(_options: TakePhotoOptions): Promise<MediaResult> {
throw this.unimplemented('takePhoto is not implemented on Web.');
async takePhoto(options: TakePhotoOptions): Promise<MediaResult> {
// eslint-disable-next-line no-async-promise-executor
return new Promise<MediaResult>(async (resolve, reject) => {
if (options.webUseInput) {
this.takePhotoCameraInputExperience(options, resolve, reject);
} else {
this.takePhotoCameraExperience(options, resolve, reject);
}
});
}

async recordVideo(_options: RecordVideoOptions): Promise<MediaResult> {

Check warning on line 34 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_options' is defined but never used
throw this.unimplemented('recordVideo is not implemented on Web.');
}

async playVideo(_options: PlayVideoOptions): Promise<void> {

Check warning on line 38 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_options' is defined but never used
throw this.unimplemented('playVideo is not implemented on Web.');
}

async chooseFromGallery(_options: ChooseFromGalleryOptions): Promise<MediaResults> {
throw this.unimplemented('chooseFromGallery is not implemented on web.');
async chooseFromGallery(options: ChooseFromGalleryOptions): Promise<MediaResults> {
// eslint-disable-next-line no-async-promise-executor
return new Promise<MediaResults>(async (resolve, reject) => {
this.galleryInputExperience(options, resolve, reject);
});
}

async editPhoto(_options: EditPhotoOptions): Promise<EditPhotoResult> {

Check warning on line 49 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_options' is defined but never used
throw this.unimplemented('editPhoto is not implemented on Web.');
}

async editURIPhoto(_options: EditURIPhotoOptions): Promise<MediaResult> {

Check warning on line 53 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_options' is defined but never used
throw this.unimplemented('editURIPhoto is not implemented on Web.');
}

Expand Down Expand Up @@ -75,7 +85,7 @@
});
}

async pickImages(_options: GalleryImageOptions): Promise<GalleryPhotos> {

Check warning on line 88 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_options' is defined but never used
// eslint-disable-next-line no-async-promise-executor
return new Promise<GalleryPhotos>(async (resolve, reject) => {
this.multipleFileInputExperience(resolve, reject);
Expand Down Expand Up @@ -129,8 +139,8 @@
input.type = 'file';
input.hidden = true;
document.body.appendChild(input);
input.addEventListener('change', (_e: any) => {

Check warning on line 142 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_e' is defined but never used
const file = input.files![0];

Check warning on line 143 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Forbidden non-null assertion
let format = 'jpeg';

if (file.type === 'image/png') {
Expand Down Expand Up @@ -168,7 +178,7 @@
cleanup();
}
});
input.addEventListener('cancel', (_e: any) => {

Check warning on line 181 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_e' is defined but never used
reject(new CapacitorException('User cancelled photos app'));
cleanup();
});
Expand Down Expand Up @@ -202,10 +212,10 @@
input.hidden = true;
input.multiple = true;
document.body.appendChild(input);
input.addEventListener('change', (_e: any) => {

Check warning on line 215 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'_e' is defined but never used
const photos = [];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < input.files!.length; i++) {

Check warning on line 218 in src/web.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Forbidden non-null assertion
const file = input.files![i];
let format = 'jpeg';

Expand Down Expand Up @@ -268,6 +278,194 @@
});
}

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<MediaResult>((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<PermissionStatus> {
if (typeof navigator === 'undefined' || !navigator.permissions) {
throw this.unavailable('Permissions API not available in this browser');
Expand Down
Loading