feat: file upload via camera

This commit is contained in:
Shariq Ansari 2024-10-14 18:58:01 +05:30
parent 998abc2cd8
commit 7afdf97697
3 changed files with 108 additions and 16 deletions

View File

@ -16,7 +16,7 @@
</template> </template>
<template #actions> <template #actions>
<div class="flex justify-between"> <div class="flex justify-between">
<div> <div class="flex gap-2">
<Button <Button
v-if="files.length" v-if="files.length"
variant="subtle" variant="subtle"
@ -25,14 +25,28 @@
@click="removeAllFiles" @click="removeAllFiles"
/> />
<Button <Button
v-if="filesUploaderArea?.showWebLink" v-if="
filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera
"
:label="__('Back to file upload')" :label="__('Back to file upload')"
@click="filesUploaderArea.showWebLink = false" @click="
() => {
filesUploaderArea.showWebLink = false
filesUploaderArea.showCamera = false
filesUploaderArea.webLink = null
filesUploaderArea.cameraImage = null
}
"
> >
<template #prefix> <template #prefix>
<FeatherIcon name="arrow-left" class="size-4" /> <FeatherIcon name="arrow-left" class="size-4" />
</template> </template>
</Button> </Button>
<Button
v-if="filesUploaderArea?.cameraImage"
:label="__('Retake')"
@click="filesUploaderArea.cameraImage = null"
/>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
@ -50,12 +64,29 @@
@click="setAllPrivate" @click="setAllPrivate"
/> />
<Button <Button
v-if="!filesUploaderArea?.showCamera"
variant="solid" variant="solid"
:label="__('Attach')" :label="__('Attach')"
:loading="fileUploadStarted" :loading="fileUploadStarted"
:disabled="disableAttachButton" :disabled="disableAttachButton"
@click="attachFiles" @click="attachFiles"
/> />
<Button
v-if="
filesUploaderArea?.showCamera && filesUploaderArea?.cameraImage
"
variant="solid"
:label="__('Upload')"
@click="() => filesUploaderArea.uploadViaCamera()"
/>
<Button
v-if="
filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage
"
variant="solid"
:label="__('Capture')"
@click="() => filesUploaderArea.captureImage()"
/>
</div> </div>
</div> </div>
</template> </template>
@ -105,6 +136,9 @@ function removeAllFiles() {
} }
const disableAttachButton = computed(() => { const disableAttachButton = computed(() => {
if (filesUploaderArea.value?.showCamera) {
return !filesUploaderArea.value.cameraImage
}
if (filesUploaderArea.value?.showWebLink) { if (filesUploaderArea.value?.showWebLink) {
return !filesUploaderArea.value.webLink return !filesUploaderArea.value.webLink
} }
@ -141,7 +175,7 @@ const fileUploadStarted = ref(false)
function attachFile(file, i) { function attachFile(file, i) {
const args = { const args = {
file: file?.fileObj || {}, fileObj: file.fileObj || {},
type: file.type, type: file.type,
private: file.private, private: file.private,
fileUrl: file.fileUrl, fileUrl: file.fileUrl,

View File

@ -2,6 +2,15 @@
<div v-if="showWebLink"> <div v-if="showWebLink">
<TextInput v-model="webLink" placeholder="https://example.com" /> <TextInput v-model="webLink" placeholder="https://example.com" />
</div> </div>
<div v-else-if="showCamera">
<video v-show="!cameraImage" ref="video" class="rounded" autoplay></video>
<canvas
v-show="cameraImage"
ref="canvas"
class="rounded"
style="width: -webkit-fill-available"
/>
</div>
<div v-else> <div v-else>
<div <div
class="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed min-h-64 text-gray-600" class="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed min-h-64 text-gray-600"
@ -38,7 +47,7 @@
<div class="mt-1">{{ __('Link') }}</div> <div class="mt-1">{{ __('Link') }}</div>
</div> </div>
<div v-if="allowTakePhoto"> <div v-if="allowTakePhoto">
<Button icon="camera" size="md" @click="captureImage" /> <Button icon="camera" size="md" @click="startCamera" />
<div class="mt-1">{{ __('Camera') }}</div> <div class="mt-1">{{ __('Camera') }}</div>
</div> </div>
</div> </div>
@ -117,7 +126,7 @@
import FileTextIcon from '@/components/Icons/FileTextIcon.vue' import FileTextIcon from '@/components/Icons/FileTextIcon.vue'
import FileAudioIcon from '@/components/Icons/FileAudioIcon.vue' import FileAudioIcon from '@/components/Icons/FileAudioIcon.vue'
import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue' import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue'
import { createToast } from '@/utils' import { createToast, dateFormat } from '@/utils'
import { FormControl, CircularProgressBar, createResource } from 'frappe-ui' import { FormControl, CircularProgressBar, createResource } from 'frappe-ui'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
@ -138,13 +147,17 @@ const fileInput = ref(null)
const isDragging = ref(false) const isDragging = ref(false)
const showWebLink = ref(false) const showWebLink = ref(false)
const showFileBrowser = ref(false) const showFileBrowser = ref(false)
const showCamera = ref(false)
const webLink = ref('') const webLink = ref('')
const cameraImage = ref(null)
const allowMultiple = ref(props.options.allowMultiple == false ? false : true) const allowMultiple = ref(props.options.allowMultiple == false ? false : true)
const disableFileBrowser = ref(props.options.disableFileBrowser || false) const disableFileBrowser = ref(props.options.disableFileBrowser || false)
const allowWebLink = ref(props.options.allowWebLink == false ? false : true) const allowWebLink = ref(props.options.allowWebLink == false ? false : true)
const allowTakePhoto = ref(props.options.allowTakePhoto || false) const allowTakePhoto = ref(
props.options.allowTakePhoto || window.navigator.mediaDevices || false,
)
const restrictions = ref(props.options.restrictions || {}) const restrictions = ref(props.options.restrictions || {})
const makeAttachmentsPublic = ref(props.options.makeAttachmentsPublic || false) const makeAttachmentsPublic = ref(props.options.makeAttachmentsPublic || false)
@ -186,15 +199,56 @@ function onFileInput(event) {
addFiles(fileInput.value.files) addFiles(fileInput.value.files)
} }
const video = ref(null)
const facingMode = ref('environment')
async function startCamera() {
showCamera.value = true
let stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: facingMode.value,
},
audio: false,
})
video.value.srcObject = stream
}
const canvas = ref(null)
function captureImage() { function captureImage() {
// const width = video.value.videoWidth
const height = video.value.videoHeight
canvas.value.width = width
canvas.value.height = height
canvas.value.getContext('2d').drawImage(video.value, 0, 0, width, height)
cameraImage.value = canvas.value.toDataURL('image/png')
}
function uploadViaCamera() {
const nowDatetime = dateFormat(new Date(), 'YYYY_MM_DD_HH_mm_ss')
let filename = `capture_${nowDatetime}.png`
urlToFile(cameraImage.value, filename, 'image/png').then((file) => {
addFiles([file])
showCamera.value = false
cameraImage.value = null
})
}
function urlToFile(url, filename, mime_type) {
return fetch(url)
.then((res) => res.arrayBuffer())
.then((buffer) => new File([buffer], filename, { type: mime_type }))
} }
function addFiles(fileArray) { function addFiles(fileArray) {
let _files = Array.from(fileArray) let _files = Array.from(fileArray)
.filter(checkRestrictions) .filter(checkRestrictions)
.map((file, i) => { .map((file, i) => {
let isImage = file.type.startsWith('image') let isImage = file.type?.startsWith('image')
let sizeKb = file.size / 1024 let sizeKb = file.size / 1024
return { return {
index: i, index: i,
@ -203,7 +257,7 @@ function addFiles(fileArray) {
cropperFile: file, cropperFile: file,
cropBoxData: null, cropBoxData: null,
type: file.type, type: file.type,
optimize: sizeKb > 200 && isImage && !file.type.includes('svg'), optimize: sizeKb > 200 && isImage && !file.type?.includes('svg'),
name: file.name, name: file.name,
doc: null, doc: null,
progress: 0, progress: 0,
@ -314,13 +368,13 @@ function convertSize(size) {
size /= 1024 size /= 1024
unitIndex++ unitIndex++
} }
return `${size.toFixed(2)} ${units[unitIndex]}` return `${size?.toFixed(2)} ${units[unitIndex]}`
} }
function fileIcon(type) { function fileIcon(type) {
if (type.startsWith('audio')) { if (type?.startsWith('audio')) {
return FileAudioIcon return FileAudioIcon
} else if (type.startsWith('video')) { } else if (type?.startsWith('video')) {
return FileVideoIcon return FileVideoIcon
} }
return FileTextIcon return FileTextIcon
@ -330,5 +384,9 @@ defineExpose({
showFileBrowser, showFileBrowser,
showWebLink, showWebLink,
webLink, webLink,
showCamera,
cameraImage,
captureImage,
uploadViaCamera,
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
interface UploadOptions { interface UploadOptions {
file?: File fileObj?: File
private?: boolean private?: boolean
fileUrl?: string fileUrl?: string
folder?: string folder?: string
@ -99,8 +99,8 @@ class FilesUploadHandler {
let formData = new FormData() let formData = new FormData()
if (options.file && file?.name) { if (options.fileObj && file?.name) {
formData.append('file', options.file, file.name) formData.append('file', options.fileObj, file.name)
} }
formData.append('is_private', options.private || false ? '1' : '0') formData.append('is_private', options.private || false ? '1' : '0')
formData.append('folder', options.folder || 'Home') formData.append('folder', options.folder || 'Home')