add camera capture functionality

This commit is contained in:
MarcZierle 2022-01-20 16:55:50 +01:00
parent 3b060df6ca
commit 2597014525
5 changed files with 335 additions and 7 deletions

View File

@ -6,7 +6,7 @@
<n-config-provider :theme="null">
<n-message-provider>
<NavBar v-if="!isHome" />
<NavBar v-if="doesntNeedNav" />
<router-view />
</n-message-provider>
</n-config-provider>
@ -34,8 +34,9 @@ export default defineComponent({
return { darkTheme }
},
computed: {
isHome() {
return this.$route.name == 'Home'
doesntNeedNav() {
let currentPage = this.$route.name
return !(currentPage == 'Home' || this.$route.name == 'CameraCapture')
}
}
})

View File

@ -3,7 +3,7 @@ import axios from 'axios'
const apiClient = axios.create({
baseURL: 'https://server.riezel.com/api/v1',
withCredentials: false,
timeout: 2000,
timeout: 10000,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
@ -52,4 +52,32 @@ export function getPhotosByGroup(group_id) {
export function getPhotoLogPDF(id) {
return apiClient.get('/generatephotolog/'+id+'/')
}
export function addNewPhotoGroup(name, date) {
return apiClient.post('/addphotogroup/', {
name,
date
})
}
export function addNewPhoto(original_image, cropped_image=null, group_id=null, ocr_text=null, legacy_id=null) {
const data = new FormData()
data.append('original_image', original_image, original_image.name)
if (cropped_image !== null)
data.append('cropped_image', cropped_image, cropped_image.name)
if (group_id !== null)
data.append('group', group_id)
if (ocr_text !== null)
data.append('ocr_text', ocr_text)
if (legacy_id !== null)
data.append('legacy_id', legacy_id)
const config = {
headers: { 'Content-Type': 'multipart/form-data' }
}
return apiClient.post('/addphoto/', data, config)
}

View File

@ -4,6 +4,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/Home.vue'
import LogsList from '../views/LogsList.vue'
import CreateLog from '../views/CreateLog.vue'
import CameraCapture from '../views/CameraCapture.vue'
const routes = [
{
@ -27,7 +28,12 @@ const routes = [
}
return {e}
}
}
},
{
path: '/capture',
name: 'CameraCapture',
component: CameraCapture
},
]
const router = createRouter({

282
src/views/CameraCapture.vue Normal file
View File

@ -0,0 +1,282 @@
<template>
<n-config-provider :theme="darkTheme">
<div id="all">
<div id="group_select">
<n-select v-model:value="selected_group" :options="groups" size="large" />
<n-button secondary type="primary" @click="showAddGroupModal = true" size="large">
<n-icon><CreateNewFolderRound /></n-icon>
</n-button>
</div>
<div id="camera">
<div id="prompt_select" v-show="!selected_group">
<n-icon size="4em"><HandPointUp/></n-icon><br/>
<h1 style="font-weight:bold;">Please select a photo group</h1>
</div>
<n-spin :show="isCameraLoading || isPhotoTaken" v-show="selected_group">
<div id="camera_preview">
<video v-show="!isPhotoTaken" ref="camera" autoplay></video>
<canvas v-show="isPhotoTaken" id="photoTaken" ref="canvas" width="100" height="100"></canvas>
</div>
</n-spin>
</div>
<div id="take_photo">
<n-space justify-content="center">
<n-button
type="primary" round size="large"
:disabled="!selected_group || isPhotoTaken || !isCameraCreated"
@click="takePhoto"
>
Take Photo
</n-button>
</n-space>
</div>
</div>
<n-modal
v-model:show="showAddGroupModal"
:mask-closable="true"
transform-origin="center"
preset="card"
title="Add new Group"
:bordered="false"
size="huge">
<n-input placeholder="Enter group name" size="large" v-model:value="new_group_name" />
<template #footer>
<n-space justify-content="end">
<n-button size="large" @click="showAddGroupModal = false">Cancel</n-button>
<n-button type="primary" size="large" @click="addNewGroup">Add Group</n-button>
</n-space>
</template>
</n-modal>
<n-modal
v-model:show="showPhotoTakenModal"
:mask-closable="false"
transform-origin="center"
preset="card"
:title="'Upload Photo to '+selected_group_label"
:bordered="false"
size="huge">
<n-space justify-content="center">
<n-image
:src="photo.src"
:width="photo.width*0.75"
/>
</n-space>
<template #footer>
<n-space justify-content="end">
<n-button size="large" @click="showPhotoTakenModal = false; isPhotoTaken = false">Back</n-button>
<n-button type="primary" size="large" @click="uploadPhoto">Upload</n-button>
</n-space>
</template>
</n-modal>
</n-config-provider>
</template>
<script>
import { useMeta } from 'vue-meta'
import { useMessage } from 'naive-ui'
import {darkTheme} from 'naive-ui'
import CreateNewFolderRound from '@vicons/material/CreateNewFolderRound'
import HandPointUp from '@vicons/fa/HandPointUp'
import { getPhotoGroups, addNewPhotoGroup, addNewPhoto } from '@/api'
export default {
components: {
CreateNewFolderRound,
HandPointUp,
},
setup() {
useMeta({
title: 'Capture new Photo',
})
const message = useMessage()
return { message }
},
data() {
return {
darkTheme,
groups: [],
selected_group: null,
showAddGroupModal: false,
new_group_name: '',
isCameraCreated: false,
isCameraLoading: true,
isPhotoTaken: false,
photo: {
width: 0,
height: 0,
src: null
},
showPhotoTakenModal: false,
}
},
beforeMount() {
this.fetchPhotoGroups()
},
mounted() {
window.scrollTo(0, 1)
},
watch: {
selected_group: function () {
if (this.isCameraCreated)
return
this.startCamera()
},
},
computed: {
selected_group_label() {
if (this.selected_group !== null) {
return this.groups.filter(group => group.value === this.selected_group)[0].label
}
return ''
},
},
methods: {
fetchPhotoGroups() {
this.groups = []
getPhotoGroups().then((response) => {
let data = response.data
for(const data_idx in data) {
this.groups.push({
label: data[data_idx].name,
value: data[data_idx].id,
})
}
}).catch((error)=>{
this.message.error('There was an error while fetching photo groups: '+error)
})
},
addNewGroup() {
if (this.new_group_name.length == 0) {
this.message.error('Please enter a new group name')
return
}
addNewPhotoGroup(this.new_group_name, null).then((response) => {
this.message.success('Added group '+this.new_group_name)
this.fetchPhotoGroups()
this.showAddGroupModal = false
this.selected_group = response.data.id
}).catch((error)=>{
this.message.error('There was an error while adding the group: '+error)
}).finally(() => {
this.new_group_name = ''
})
},
startCamera() {
this.isCameraLoading = true
const constraints = (window.constraints = {
audio: false,
video: true
})
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
this.isCameraLoading = false
this.$refs.camera.srcObject = stream
this.isCameraCreated = true
this.photo.width = document.getElementById('camera_preview').offsetWidth
this.$refs.camera.width = this.photo.width
this.photo.height = document.getElementById('camera_preview').offsetHeight
}).catch(error => {
this.message.error('Cannot load device camera! '+error)
})
},
takePhoto() {
this.isPhotoTaken = true
this.photo.height = document.getElementById('camera_preview').offsetHeight
this.$refs.canvas.width = this.photo.width
this.$refs.canvas.height = this.photo.height
const context = this.$refs.canvas.getContext('2d')
context.drawImage(
this.$refs.camera,
0, 0,
this.photo.width,
this.photo.height)
this.photo.src = this.$refs.canvas.toDataURL('image/png')//.replace('image/png', 'image/octet-stream')
this.showPhotoTakenModal = true
},
uploadPhoto() {
const dataURLtoFile = (dataurl, filename) => {
const arr = dataurl.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n) {
u8arr[n - 1] = bstr.charCodeAt(n - 1)
n -= 1 // to make eslint happy
}
return new File([u8arr], filename, { type: mime })
}
const file = dataURLtoFile(this.photo.src, 'upload.png')
addNewPhoto(file, null, this.selected_group)
},
},
}
</script>
<style scoped>
html, body {
margin: 0;
padding: 0;
}
#all {
background-color: #111;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-between;
overflow: hidden;
color: white;
}
#group_select {
display: flex;
}
#prompt_select {
display: flex;
flex-direction: column;
text-align: center;
}
#prompt_select .n-icon {
margin: auto;
}
#take_photo {
height: 15%;
background-color: #222;
display: flex;
justify-content: center;
}
#take_photo .n-button {
margin-top: 30%;
}
</style>

View File

@ -28,7 +28,14 @@
<n-button type="info">+ New</n-button>
</router-link>
</n-space>
<n-button>Crop Photos</n-button>
<n-space>
<router-link :to="{name: 'CameraCapture'}">
<n-button secondary circle type="info">
<n-icon><CameraAdd20Filled /></n-icon>
</n-button>
</router-link>
<n-button>Crop Photos</n-button>
</n-space>
</n-space>
</n-space>
</div>
@ -36,9 +43,13 @@
</template>
<script>
import CameraAdd20Filled from '@vicons/fluent/CameraAdd20Filled'
export default {
name: 'HomeView'
name: 'HomeView',
components: {
CameraAdd20Filled
}
}
</script>