20334 implement desktop camera upload feature (#335)
* wip: camera upload * Handle camera permissions * remove console logs * fix camera theme, hide on small screen, * video sizing on camera upload * camera feature minor fixes * Package.json update --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
a32c183758
commit
659dc5086e
|
|
@ -3,14 +3,23 @@
|
||||||
<template #label>
|
<template #label>
|
||||||
<slot name="label" />
|
<slot name="label" />
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="cameraUpload && isInWebcam" class="hidden sm:block w-full min-h-40">
|
||||||
<div class="flex w-full items-center justify-center transition-colors duration-40" :class="[{
|
<camera-upload v-if="cameraUpload" @uploadImage="cameraFileUpload" @stopWebcam="isInWebcam=false" :theme="theme"/>
|
||||||
'!cursor-not-allowed': disabled, 'cursor-pointer': !disabled,
|
</div>
|
||||||
|
<div v-else class="flex flex-col w-full items-center justify-center transition-colors duration-40"
|
||||||
|
:class="[{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled,
|
||||||
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
|
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
|
||||||
['hover:' + theme.fileInput.inputHover.light + ' dark:hover:' + theme.fileInput.inputHover.dark]: !loading
|
['hover:'+theme.fileInput.inputHover.light +' dark:hover:'+theme.fileInput.inputHover.dark]: !loading}, theme.fileInput.input]"
|
||||||
}, theme.fileInput.input]" @dragover.prevent="uploadDragoverEvent = true"
|
@dragover.prevent="uploadDragoverEvent=true"
|
||||||
@dragleave.prevent="uploadDragoverEvent = false" @drop.prevent="onUploadDropEvent" @click="openFileUpload">
|
@dragleave.prevent="uploadDragoverEvent=false"
|
||||||
<div v-if="loading" class="text-gray-600 dark:text-gray-400">
|
@drop.prevent="onUploadDropEvent"
|
||||||
|
@click="openFileUpload"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center justify-center">
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
<Loader class="mx-auto h-6 w-6" />
|
<Loader class="mx-auto h-6 w-6" />
|
||||||
<p class="mt-2 text-center text-sm text-gray-500">
|
<p class="mt-2 text-center text-sm text-gray-500">
|
||||||
Uploading your file...
|
Uploading your file...
|
||||||
|
|
@ -43,6 +52,15 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full items-center justify-center mt-2 hidden sm:flex">
|
||||||
|
<open-form-button native-type="button" :loading="loading" :theme="theme" :color="color" class="py-2 p-1 px-2" @click.stop="openWebcam" v-if="cameraUpload">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
|
||||||
|
</svg>
|
||||||
|
</open-form-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #help>
|
<template #help>
|
||||||
<slot name="help" />
|
<slot name="help" />
|
||||||
|
|
@ -57,15 +75,18 @@
|
||||||
import { inputProps, useFormInput } from './useFormInput.js'
|
import { inputProps, useFormInput } from './useFormInput.js'
|
||||||
import InputWrapper from './components/InputWrapper.vue'
|
import InputWrapper from './components/InputWrapper.vue'
|
||||||
import UploadedFile from './components/UploadedFile.vue'
|
import UploadedFile from './components/UploadedFile.vue'
|
||||||
|
import OpenFormButton from '../open/forms/OpenFormButton.vue'
|
||||||
|
import CameraUpload from './components/CameraUpload.vue'
|
||||||
import { storeFile } from "~/lib/file-uploads.js"
|
import { storeFile } from "~/lib/file-uploads.js"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileInput',
|
name: 'FileInput',
|
||||||
components: { InputWrapper, UploadedFile },
|
components: { InputWrapper, UploadedFile, OpenFormButton },
|
||||||
mixins: [],
|
mixins: [],
|
||||||
props: {
|
props: {
|
||||||
...inputProps,
|
...inputProps,
|
||||||
multiple: { type: Boolean, default: true },
|
multiple: { type: Boolean, default: true },
|
||||||
|
cameraUpload: { type: Boolean, default: false },
|
||||||
mbLimit: { type: Number, default: 5 },
|
mbLimit: { type: Number, default: 5 },
|
||||||
accept: { type: String, default: '' },
|
accept: { type: String, default: '' },
|
||||||
moveToFormAssets: { type: Boolean, default: false }
|
moveToFormAssets: { type: Boolean, default: false }
|
||||||
|
|
@ -80,7 +101,8 @@ export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
files: [],
|
files: [],
|
||||||
uploadDragoverEvent: false,
|
uploadDragoverEvent: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
isInWebcam:false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -162,6 +184,17 @@ export default {
|
||||||
this.uploadFileToServer(files.item(i))
|
this.uploadFileToServer(files.item(i))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openWebcam(){
|
||||||
|
if(!this.cameraUpload){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isInWebcam = true;
|
||||||
|
},
|
||||||
|
cameraFileUpload(file){
|
||||||
|
this.isInWebcam = false
|
||||||
|
this.isUploading = false;
|
||||||
|
this.uploadFileToServer(file)
|
||||||
|
},
|
||||||
uploadFileToServer(file) {
|
uploadFileToServer(file) {
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative border">
|
||||||
|
<video id="webcam" autoplay playsinline :class="[{ 'hidden': !isCapturing },theme.fileInput.cameraInput]" width="1280" height="720"></video>
|
||||||
|
<canvas id="canvas" :class="{ 'hidden': !capturedImage }"></canvas>
|
||||||
|
<div v-if="cameraPermissionStatus === 'allowed'" class="absolute inset-x-0 grid place-content-center bottom-2">
|
||||||
|
<div class=" p-2 px-4 flex items-center justify-center text-xs space-x-2" v-if="isCapturing">
|
||||||
|
<span class="cursor-pointer rounded-full w-14 h-14 border-2 grid place-content-center"
|
||||||
|
@click="processCapturedImage">
|
||||||
|
<span class="cursor-pointer bg-gray-100 rounded-full w-10 h-10 grid place-content-center">
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-white cursor-pointer" @click="cancelCamera">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
stroke="currentColor" class="w-8 h-8">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="cameraPermissionStatus === 'blocked'"
|
||||||
|
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||||
|
@click="openCameraUpload">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||||
|
class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-center font-bold">
|
||||||
|
Allow Camera Permission
|
||||||
|
</p>
|
||||||
|
<p class="text-xs">You need to allow camera permission before you can take pictures. Go to browser settings to enable camera permission on this page.</p>
|
||||||
|
<button class="text-xs p-1 px-2 bg-blue-600 rounded" type="button" @click.stop="cancelCamera">Got it!</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="cameraPermissionStatus === 'loading'"
|
||||||
|
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||||
|
>
|
||||||
|
<div class="w-6 h-6">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else
|
||||||
|
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||||
|
@click="openCameraUpload">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M12 18.75H4.5a2.25 2.25 0 0 1-2.25-2.25V9m12.841 9.091L16.5 19.5m-1.409-1.409c.407-.407.659-.97.659-1.591v-9a2.25 2.25 0 0 0-2.25-2.25h-9c-.621 0-1.184.252-1.591.659m12.182 12.182L2.909 5.909M1.5 4.5l1.409 1.409" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<p class="text-center font-bold">
|
||||||
|
Camera Device Error
|
||||||
|
</p>
|
||||||
|
<p class="text-xs">An unknown error occurred when trying to start Webcam device.</p>
|
||||||
|
<button class="text-xs p-1 px-2 bg-blue-600 rounded" type="button" @click.stop="cancelCamera">Go back</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Webcam from 'webcam-easy';
|
||||||
|
import { themes } from '~/lib/forms/form-themes.js'
|
||||||
|
export default {
|
||||||
|
name: 'FileInput',
|
||||||
|
props:{
|
||||||
|
theme: { type: Object, default: () => themes.default }
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
webcam: null,
|
||||||
|
isCapturing: false,
|
||||||
|
capturedImage: null,
|
||||||
|
cameraPermissionStatus: 'loading',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
videoDisplay() {
|
||||||
|
return this.isCapturing ? '' : 'hidden';
|
||||||
|
},
|
||||||
|
canvasDisplay() {
|
||||||
|
return (!this.isCapturing && this.capturedImage) ? '' : 'hidden'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const webcamElement = document.getElementById('webcam');
|
||||||
|
const canvasElement = document.getElementById('canvas');
|
||||||
|
this.webcam = new Webcam(webcamElement, 'user', canvasElement);
|
||||||
|
this.openCameraUpload()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
openCameraUpload() {
|
||||||
|
this.isCapturing = true;
|
||||||
|
this.capturedImage = null;
|
||||||
|
this.webcam.start()
|
||||||
|
.then(result => {
|
||||||
|
this.cameraPermissionStatus = 'allowed';
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
if(err.toString() === 'NotAllowedError: Permission denied'){
|
||||||
|
this.cameraPermissionStatus = 'blocked';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cameraPermissionStatus = 'unknown';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancelCamera() {
|
||||||
|
this.isCapturing = false;
|
||||||
|
this.capturedImage = null;
|
||||||
|
this.webcam.stop()
|
||||||
|
this.$emit('stopWebcam')
|
||||||
|
},
|
||||||
|
processCapturedImage() {
|
||||||
|
this.capturedImage = this.webcam.snap();
|
||||||
|
this.isCapturing = false;
|
||||||
|
this.webcam.stop()
|
||||||
|
const byteCharacters = atob(this.capturedImage.split(',')[1]);
|
||||||
|
const byteArrays = [];
|
||||||
|
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||||
|
const slice = byteCharacters.slice(offset, offset + 512);
|
||||||
|
|
||||||
|
const byteNumbers = new Array(slice.length);
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
byteArrays.push(byteArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Blob from binary data
|
||||||
|
const blob = new Blob(byteArrays, { type: 'image/png' });
|
||||||
|
const filename = Date.now()
|
||||||
|
// Create a File object from the Blob
|
||||||
|
const file = new File([blob], `${filename}.png`, { type: 'image/png' });
|
||||||
|
this.$emit('uploadImage', file)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -272,6 +272,7 @@ export default {
|
||||||
}
|
}
|
||||||
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
|
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
|
||||||
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
|
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
|
||||||
|
inputProperties.cameraUpload = (field.camera_upload !== undefined && field.camera_upload)
|
||||||
let maxFileSize = (this.form?.workspace && this.form?.workspace.max_file_size) ? this.form?.workspace?.max_file_size : 10
|
let maxFileSize = (this.form?.workspace && this.form?.workspace.max_file_size) ? this.form?.workspace?.max_file_size : 10
|
||||||
if (field?.max_file_size > 0) {
|
if (field?.max_file_size > 0) {
|
||||||
maxFileSize = Math.min(field.max_file_size, maxFileSize)
|
maxFileSize = Math.min(field.max_file_size, maxFileSize)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,11 @@
|
||||||
<v-checkbox v-model="field.multiple" class="mt-3" :name="field.id + '_multiple'">
|
<v-checkbox v-model="field.multiple" class="mt-3" :name="field.id + '_multiple'">
|
||||||
Allow multiple files
|
Allow multiple files
|
||||||
</v-checkbox>
|
</v-checkbox>
|
||||||
|
<v-checkbox v-model="field.camera_upload" class="mt-3"
|
||||||
|
:name="field.id+'_camera_upload'"
|
||||||
|
>
|
||||||
|
Allow Camera uploads
|
||||||
|
</v-checkbox>
|
||||||
<text-input name="allowed_file_types" class="mt-3" :form="field" label="Allowed file types"
|
<text-input name="allowed_file_types" class="mt-3" :form="field" label="Allowed file types"
|
||||||
placeholder="jpg,jpeg,png,gif" help="Comma separated values, leave blank to allow all file types" />
|
placeholder="jpg,jpeg,png,gif" help="Comma separated values, leave blank to allow all file types" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export const themes = {
|
||||||
},
|
},
|
||||||
fileInput: {
|
fileInput: {
|
||||||
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded-lg',
|
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded-lg',
|
||||||
|
cameraInput: 'min-h-40 rounded-lg',
|
||||||
inputHover: {
|
inputHover: {
|
||||||
light: 'bg-neutral-50',
|
light: 'bg-neutral-50',
|
||||||
dark: 'bg-notion-dark-light'
|
dark: 'bg-notion-dark-light'
|
||||||
|
|
@ -83,6 +84,7 @@ export const themes = {
|
||||||
},
|
},
|
||||||
fileInput: {
|
fileInput: {
|
||||||
input: 'min-h-40 border border-dashed border-gray-300 p-4',
|
input: 'min-h-40 border border-dashed border-gray-300 p-4',
|
||||||
|
cameraInput: 'min-h-40',
|
||||||
inputHover: {
|
inputHover: {
|
||||||
light: 'bg-neutral-50',
|
light: 'bg-neutral-50',
|
||||||
dark: 'bg-notion-dark-light'
|
dark: 'bg-notion-dark-light'
|
||||||
|
|
@ -127,6 +129,7 @@ export const themes = {
|
||||||
},
|
},
|
||||||
fileInput: {
|
fileInput: {
|
||||||
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded bg-notion-input-background',
|
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded bg-notion-input-background',
|
||||||
|
cameraInput: 'min-h-40 rounded',
|
||||||
inputHover: {
|
inputHover: {
|
||||||
light: 'bg-neutral-50',
|
light: 'bg-neutral-50',
|
||||||
dark: 'bg-notion-dark-light'
|
dark: 'bg-notion-dark-light'
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@
|
||||||
"vue-notion": "^3.0.0-beta.1",
|
"vue-notion": "^3.0.0-beta.1",
|
||||||
"vue-signature-pad": "^3.0.2",
|
"vue-signature-pad": "^3.0.2",
|
||||||
"vue3-editor": "^0.1.1",
|
"vue3-editor": "^0.1.1",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next",
|
||||||
|
"webcam-easy": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/devtools": "latest",
|
"@nuxt/devtools": "latest",
|
||||||
|
|
@ -12343,6 +12344,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
|
||||||
"integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw=="
|
"integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
|
||||||
|
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ultrahtml": {
|
"node_modules/ultrahtml": {
|
||||||
"version": "1.5.3",
|
"version": "1.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz",
|
||||||
|
|
@ -13510,6 +13522,14 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webcam-easy": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webcam-easy/-/webcam-easy-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-L2/tXBpWCdZAXI7s5SWB0BICiDATih9OHIhIZpkCWJX4lENF0UTsnqztAb+c0XPy63HoiLUpCHRhdMFaospzGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"uglify-js": "^3.17.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
"vue-notion": "^3.0.0-beta.1",
|
"vue-notion": "^3.0.0-beta.1",
|
||||||
"vue-signature-pad": "^3.0.2",
|
"vue-signature-pad": "^3.0.2",
|
||||||
"vue3-editor": "^0.1.1",
|
"vue3-editor": "^0.1.1",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next",
|
||||||
|
"webcam-easy": "^1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue