@ -40,11 +40,6 @@
} "
class = "max-h-[90vh] max-w-[90vw] object-contain"
/ >
<!-- This gives a round cropper .
: presetMode = " {
mode : 'round' ,
} "
-- >
< / div >
< div v-else >
< div class = "flex justify-center" >
@ -74,87 +69,67 @@
< / button >
< / div >
< / div >
< div v -else ref = "cameraContainer" >
<!--
Camera "resolution" doesn ' t change how it shows on screen but rather stretches the result ,
eg . the following which just stretches it vertically :
: resolution = "{ width: 375, height: 812 }"
-- >
< camera
ref = "camera"
facing - mode = "environment"
autoplay
@ started = "cameraStarted()"
>
< div
class = "absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
< div v-else >
< div class = "flex flex-col items-center justify-center gap-4 p-4" >
< button
class = "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@ click = "takePhoto"
>
< button
class = "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@ click = "takeImage()"
>
< font -awesome icon = "camera" class = "w-[1em]" > < / f o n t - a w e s o m e >
< / button >
< / div >
< div
class = "absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
< font -awesome icon = "camera" class = "w-[1em]" / >
< / button >
< button
class = "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@ click = "pickPhoto"
>
< button
class = "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@ click = "swapMirrorClass()"
>
< font -awesome icon = "left-right" class = "w-[1em]" > < / f o n t - a w e s o m e >
< / button >
< / div >
< div v-if ="numDevices > 1" class="absolute bottom-2 right-4" >
< button
class = "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@ click = "switchCamera()"
>
< font -awesome icon = "rotate" class = "w-[1em]" > < / f o n t - a w e s o m e >
< / button >
< / div >
< / camera >
< font -awesome icon = "image" class = "w-[1em]" / >
< / button >
< / div >
< / div >
< / div >
< / div >
< / template >
< script lang = "ts" >
/ * *
* PhotoDialog . vue - Cross - platform photo capture and selection component
*
* This component provides a unified interface for taking photos and selecting images
* across different platforms using the PlatformService .
*
* @ author Matthew Raymer
* @ file PhotoDialog . vue
* /
import axios from "axios" ;
import Camera from "simple-vue-camera" ;
import { Component , Vue } from "vue-facing-decorator" ;
import VuePictureCropper , { cropper } from "vue-picture-cropper" ;
import { DEFAULT_IMAGE_API_SERVER , NotificationIface } from "../constants/app" ;
import { retrieveSettingsForActiveAccount } from "../db/index" ;
import { accessToken } from "../libs/crypto" ;
import { logger } from "../utils/logger" ;
import { PlatformServiceFactory } from "../services/PlatformServiceFactory" ;
@ Component ( { components : { Camera , VuePictureCropper } } )
@ Component ( { components : { VuePictureCropper } } )
export default class PhotoDialog extends Vue {
$notify ! : ( notification : NotificationIface , timeout ? : number ) => void ;
activeDeviceNumber = 0 ;
activeDid = "" ;
blob ? : Blob ;
claimType = "" ;
crop = false ;
fileName ? : string ;
mirror = false ;
numDevices = 0 ;
setImageCallback : ( arg : string ) => void = ( ) => { } ;
showRetry = true ;
uploading = false ;
visible = false ;
private platformService = PlatformServiceFactory . getInstance ( ) ;
URL = window . URL || window . webkitURL ;
async mounted ( ) {
try {
const settings = await retrieveSettingsForActiveAccount ( ) ;
this . activeDid = settings . activeDid || "" ;
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} catch ( err : any ) {
logger . error ( "Error retrieving settings from database:" , err ) ;
this . $notify (
@ -173,7 +148,7 @@ export default class PhotoDialog extends Vue {
setImageFn : ( arg : string ) => void ,
claimType : string ,
crop ? : boolean ,
blob ? : Blob , / / f o r i m a g e u p l o a d , j u s t t o u s e t h e c r o p p i n g f u n c t i o n
blob ? : Blob ,
inputFileName ? : string ,
) {
this . visible = true ;
@ -187,7 +162,6 @@ export default class PhotoDialog extends Vue {
if ( blob ) {
this . blob = blob ;
this . fileName = inputFileName ;
/ / d o e s n ' t m a k e s e n s e t o r e t r y t h e f i l e u p l o a d ; t h e y c a n c a n c e l i f t h e y p i c k e d t h e w r o n g o n e
this . showRetry = false ;
} else {
this . blob = undefined ;
@ -205,85 +179,35 @@ export default class PhotoDialog extends Vue {
this . blob = undefined ;
}
async cameraStarted ( ) {
const cameraComponent = this . $refs . camera as InstanceType < typeof Camera > ;
if ( cameraComponent ) {
this . numDevices = ( await cameraComponent . devices ( [ "videoinput" ] ) ) . length ;
this . mirror = cameraComponent . facingMode === "user" ;
/ / f i g u r e o u t w h i c h d e v i c e i s a c t i v e
const currentDeviceId = cameraComponent . currentDeviceID ( ) ;
const devices = await cameraComponent . devices ( [ "videoinput" ] ) ;
this . activeDeviceNumber = devices . findIndex (
( device ) => device . deviceId === currentDeviceId ,
) ;
async takePhoto ( ) {
try {
const result = await this . platformService . takePicture ( ) ;
this . blob = result . blob ;
this . fileName = result . fileName ;
} catch ( error ) {
logger . error ( "Error taking picture:" , error ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : "Failed to take picture. Please try again." ,
} , 5000 ) ;
}
}
async switchCamera ( ) {
const cameraComponent = this . $refs . camera as InstanceType < typeof Camera > ;
this . activeDeviceNumber = ( this . activeDeviceNumber + 1 ) % this . numDevices ;
const devices = await cameraComponent ? . devices ( [ "videoinput" ] ) ;
await cameraComponent ? . changeCamera (
devices [ this . activeDeviceNumber ] . deviceId ,
) ;
}
async takeImage ( /* payload: MouseEvent */ ) {
const cameraComponent = this . $refs . camera as InstanceType < typeof Camera > ;
/ * *
* This logic to set the image height & width correctly .
* Without it , the portrait orientation ends up with an image that is stretched horizontally .
* Note that it ' s the same with raw browser Javascript ; see the "drawImage" example below .
* Now that I 've done it, I can' t explain why it works .
* /
let imageHeight = cameraComponent ? . resolution ? . height ;
let imageWidth = cameraComponent ? . resolution ? . width ;
const initialImageRatio = imageWidth / imageHeight ;
const windowRatio = window . innerWidth / window . innerHeight ;
if ( initialImageRatio > 1 && windowRatio < 1 ) {
/ / t h e i m a g e i s w i d e r t h a n i t i s t a l l , a n d t h e w i n d o w i s t a l l e r t h a n i t i s w i d e
/ / F o r s o m e r e a s o n , m o b i l e i n p o r t r a i t o r i e n t a t i o n r e n d e r s a h o r i z o n t a l l y - s t r e t c h e d i m a g e .
/ / W e ' r e g o n n a f o r c e i t o p p o s i t e .
imageHeight = cameraComponent ? . resolution ? . width ;
imageWidth = cameraComponent ? . resolution ? . height ;
} else if ( initialImageRatio < 1 && windowRatio > 1 ) {
/ / t h e i m a g e i s t a l l e r t h a n i t i s w i d e , a n d t h e w i n d o w i s w i d e r t h a n i t i s t a l l
/ / H a v e n ' t s e e n t h i s h a p p e n , b u t w e ' l l d o i t j u s t i n c a s e .
imageHeight = cameraComponent ? . resolution ? . width ;
imageWidth = cameraComponent ? . resolution ? . height ;
}
const newImageRatio = imageWidth / imageHeight ;
if ( newImageRatio < windowRatio ) {
/ / t h e i m a g e i s a t a l l e r r a t i o t h a n t h e w i n d o w , s o f i t t h e h e i g h t f i r s t
imageHeight = window . innerHeight / 2 ;
imageWidth = imageHeight * newImageRatio ;
} else {
/ / t h e i m a g e i s a w i d e r r a t i o t h a n t h e w i n d o w , s o f i t t h e w i d t h f i r s t
imageWidth = window . innerWidth / 2 ;
imageHeight = imageWidth / newImageRatio ;
}
/ / T h e r e s o l u t i o n i s o n l y n e c e s s a r y b e c a u s e o f t h a t m o b i l e p o r t r a i t - o r i e n t a t i o n c a s e .
/ / T h e m o b i l e e m u l a t i o n i n a b r o w s e r s h o w s s o m e t h i n g s t r e t c h e d v e r t i c a l l y , b u t r e a l d e v i c e s w o r k f i n e .
this . blob =
( await cameraComponent ? . snapshot ( {
height : imageHeight ,
width : imageWidth ,
} ) ) || undefined ;
/ / p n g i s d e f a u l t
this . fileName = "snapshot.png" ;
if ( ! this . blob ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : "There was an error taking the picture. Please try again." ,
} ,
5000 ,
) ;
return ;
async pickPhoto ( ) {
try {
const result = await this . platformService . pickImage ( ) ;
this . blob = result . blob ;
this . fileName = result . fileName ;
} catch ( error ) {
logger . error ( "Error picking image:" , error ) ;
this . $notify ( {
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : "Failed to pick image. Please try again." ,
} , 5000 ) ;
}
}
@ -295,51 +219,6 @@ export default class PhotoDialog extends Vue {
this . blob = undefined ;
}
/ * * * *
Here ' s an approach to photo capture without a library . It has similar quirks .
Now that we 've fixed styling for simple-vue-camera, it' s not critical to refactor . Maybe someday .
< button id = "start-camera" @click ="cameraClicked" > Start Camera < / button >
< video id = "video" width = "320" height = "240" autoplay > < / video >
< button id = "snap-photo" @click ="photoSnapped" > Snap Photo < / button >
< canvas id = "canvas" width = "320" height = "240" > < / canvas >
async cameraClicked ( ) {
const video = document . querySelector ( "#video" ) ;
const stream = await navigator . mediaDevices . getUserMedia ( {
video : true ,
audio : false ,
} ) ;
if ( video instanceof HTMLVideoElement ) {
video . srcObject = stream ;
}
}
photoSnapped ( ) {
const video = document . querySelector ( "#video" ) ;
const canvas = document . querySelector ( "#canvas" ) ;
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
? . getContext ( "2d" )
? . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ;
/ / . . . o r s e t t h e b l o b :
/ / c a n v a s ? . t o B l o b (
/ / ( b l o b ) = > {
/ / t h i s . b l o b = b l o b ;
/ / } ,
/ / " i m a g e / j p e g " ,
/ / 1 ,
/ / ) ;
/ / d a t a u r l o f t h e i m a g e
const image_data_url = canvas ? . toDataURL ( "image/jpeg" ) ;
}
}
* * * * /
async uploadImage ( ) {
this . uploading = true ;
@ -350,11 +229,9 @@ export default class PhotoDialog extends Vue {
const token = await accessToken ( this . activeDid ) ;
const headers = {
Authorization : "Bearer " + token ,
/ / a x i o s f i l l s i n C o n t e n t - T y p e o f m u l t i p a r t / f o r m - d a t a
} ;
const formData = new FormData ( ) ;
if ( ! this . blob ) {
/ / y e a h , t h i s s h o u l d n e v e r h a p p e n , b u t i t h e l p s w i t h s u b s e q u e n t t y p e c h e c k i n g
this . $notify (
{
group : "alert" ,
@ -367,7 +244,7 @@ export default class PhotoDialog extends Vue {
this . uploading = false ;
return ;
}
formData . append ( "image" , this . blob , this . fileName || "sna ps hot.pn g" ) ;
formData . append ( "image" , this . blob , this . fileName || "photo .j pg" ) ;
formData . append ( "claimType" , this . claimType ) ;
try {
if (
@ -402,17 +279,6 @@ export default class PhotoDialog extends Vue {
this . blob = undefined ;
}
}
swapMirrorClass ( ) {
this . mirror = ! this . mirror ;
if ( this . mirror ) {
( this . $refs . cameraContainer as HTMLElement ) . classList . add ( "mirror-video" ) ;
} else {
( this . $refs . cameraContainer as HTMLElement ) . classList . remove (
"mirror-video" ,
) ;
}
}
}
< / script >
@ -438,12 +304,4 @@ export default class PhotoDialog extends Vue {
width : 100 % ;
max - width : 700 px ;
}
. mirror - video {
transform : scaleX ( - 1 ) ;
- webkit - transform : scaleX ( - 1 ) ; /* For Safari */
- moz - transform : scaleX ( - 1 ) ; /* For Firefox */
- ms - transform : scaleX ( - 1 ) ; /* For IE */
- o - transform : scaleX ( - 1 ) ; /* For Opera */
}
< / style >