mirror of
https://github.com/MarcZierle/photo-log-frontend.git
synced 2025-04-07 13:04:37 +00:00
add camera capture functionality
This commit is contained in:
parent
3b060df6ca
commit
2597014525
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
@ -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
282
src/views/CameraCapture.vue
Normal 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>
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user