@ -72,43 +72,148 @@ export class WebPlatformService implements PlatformService {
}
/ * *
* Opens a file input dialog configured for camera capture .
* Creates a temporary file input element to access the device camera .
* Opens the device camera for photo capture on desktop browsers using getUserMedia .
* On mobile browsers , uses file input with capture attribute .
* Falls back to file input if getUserMedia is not available or fails .
*
* @returns Promise resolving to the captured image data
* @throws Error if image capture fails or no image is selected
*
* @remarks
* Uses the 'capture' attribute to prefer the device camera .
* Falls back to file selection if camera is not available .
* Processes the captured image to ensure consistent format .
* /
async takePicture ( ) : Promise < ImageResult > {
return new Promise ( ( resolve , reject ) = > {
const input = document . createElement ( "input" ) ;
input . type = "file" ;
input . accept = "image/*" ;
input . capture = "environment" ;
const isMobile = /iPhone|iPad|iPod|Android/i . test ( navigator . userAgent ) ;
const hasGetUserMedia = ! ! ( navigator . mediaDevices && navigator . mediaDevices . getUserMedia ) ;
input . onchange = async ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ] ;
if ( file ) {
try {
const blob = await this . processImageFile ( file ) ;
resolve ( {
blob ,
fileName : file.name || "photo.jpg" ,
} ) ;
} catch ( error ) {
logger . error ( "Error processing camera image:" , error ) ;
reject ( new Error ( "Failed to process camera image" ) ) ;
// If on mobile, use file input with capture attribute (existing behavior)
if ( isMobile || ! hasGetUserMedia ) {
return new Promise ( ( resolve , reject ) = > {
const input = document . createElement ( "input" ) ;
input . type = "file" ;
input . accept = "image/*" ;
input . capture = "environment" ;
input . onchange = async ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ] ;
if ( file ) {
try {
const blob = await this . processImageFile ( file ) ;
resolve ( {
blob ,
fileName : file.name || "photo.jpg" ,
} ) ;
} catch ( error ) {
logger . error ( "Error processing camera image:" , error ) ;
reject ( new Error ( "Failed to process camera image" ) ) ;
}
} else {
reject ( new Error ( "No image captured" ) ) ;
}
} else {
reject ( new Error ( "No image captured" ) ) ;
} ;
input . click ( ) ;
} ) ;
}
// Desktop: Use getUserMedia for webcam capture
return new Promise ( async ( resolve , reject ) = > {
let stream : MediaStream | null = null ;
let video : HTMLVideoElement | null = null ;
let captureButton : HTMLButtonElement | null = null ;
let overlay : HTMLDivElement | null = null ;
let cleanup = ( ) = > {
if ( stream ) {
stream . getTracks ( ) . forEach ( ( track ) = > track . stop ( ) ) ;
}
if ( video && video . parentNode ) video . parentNode . removeChild ( video ) ;
if ( captureButton && captureButton . parentNode ) captureButton . parentNode . removeChild ( captureButton ) ;
if ( overlay && overlay . parentNode ) overlay . parentNode . removeChild ( overlay ) ;
} ;
try {
stream = await navigator . mediaDevices . getUserMedia ( { video : { facingMode : "user" } } ) ;
// Create overlay for video and button
overlay = document . createElement ( "div" ) ;
overlay . style . position = "fixed" ;
overlay . style . top = "0" ;
overlay . style . left = "0" ;
overlay . style . width = "100vw" ;
overlay . style . height = "100vh" ;
overlay . style . background = "rgba(0,0,0,0.8)" ;
overlay . style . display = "flex" ;
overlay . style . flexDirection = "column" ;
overlay . style . justifyContent = "center" ;
overlay . style . alignItems = "center" ;
overlay . style . zIndex = "9999" ;
input . click ( ) ;
video = document . createElement ( "video" ) ;
video . autoplay = true ;
video . playsInline = true ;
video . style . maxWidth = "90vw" ;
video . style . maxHeight = "70vh" ;
video . srcObject = stream ;
overlay . appendChild ( video ) ;
captureButton = document . createElement ( "button" ) ;
captureButton . textContent = "Capture Photo" ;
captureButton . style . marginTop = "2rem" ;
captureButton . style . padding = "1rem 2rem" ;
captureButton . style . fontSize = "1.2rem" ;
captureButton . style . background = "#2563eb" ;
captureButton . style . color = "white" ;
captureButton . style . border = "none" ;
captureButton . style . borderRadius = "0.5rem" ;
captureButton . style . cursor = "pointer" ;
overlay . appendChild ( captureButton ) ;
document . body . appendChild ( overlay ) ;
captureButton . onclick = async ( ) = > {
try {
// Create a canvas to capture the frame
const canvas = document . createElement ( "canvas" ) ;
canvas . width = video ! . videoWidth ;
canvas . height = video ! . videoHeight ;
const ctx = canvas . getContext ( "2d" ) ;
ctx ? . drawImage ( video ! , 0 , 0 , canvas . width , canvas . height ) ;
canvas . toBlob ( ( blob ) = > {
cleanup ( ) ;
if ( blob ) {
resolve ( {
blob ,
fileName : ` photo_ ${ Date . now ( ) } .jpg ` ,
} ) ;
} else {
reject ( new Error ( "Failed to capture image from webcam" ) ) ;
}
} , "image/jpeg" , 0.95 ) ;
} catch ( err ) {
cleanup ( ) ;
reject ( err ) ;
}
} ;
} catch ( error ) {
cleanup ( ) ;
logger . error ( "Error accessing webcam:" , error ) ;
// Fallback to file input
const input = document . createElement ( "input" ) ;
input . type = "file" ;
input . accept = "image/*" ;
input . onchange = async ( e ) = > {
const file = ( e . target as HTMLInputElement ) . files ? . [ 0 ] ;
if ( file ) {
try {
const blob = await this . processImageFile ( file ) ;
resolve ( {
blob ,
fileName : file.name || "photo.jpg" ,
} ) ;
} catch ( error ) {
logger . error ( "Error processing fallback image:" , error ) ;
reject ( new Error ( "Failed to process fallback image" ) ) ;
}
} else {
reject ( new Error ( "No image selected" ) ) ;
}
} ;
input . click ( ) ;
}
} ) ;
}
@ -228,4 +333,14 @@ export class WebPlatformService implements PlatformService {
// Web platform can handle deep links through URL parameters
return Promise . resolve ( ) ;
}
/ * *
* Not supported in web platform .
* @param _fileName - Unused fileName parameter
* @param _content - Unused content parameter
* @throws Error indicating file system access is not available
* /
async writeAndShareFile ( _fileName : string , _content : string ) : Promise < void > {
throw new Error ( "File system access not available in web platform" ) ;
}
}