Set up the Capture view

This commit is contained in:
Bubka 2023-10-31 15:32:01 +01:00
parent b516fd9c33
commit 7cf8a70743
5 changed files with 246 additions and 2 deletions

64
package-lock.json generated
View File

@ -26,6 +26,7 @@
"vite": "^4.4.9",
"vue": "^3.3.4",
"vue-draggable-plus": "^0.2.6",
"vue-qrcode-reader": "^5.4.0",
"vue-router": "^4.2.4"
}
},
@ -603,6 +604,18 @@
}
}
},
"node_modules/@types/dom-webcodecs": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.9.tgz",
"integrity": "sha512-lOqlovxh4zB7p59rJwej8XG3uo0kv+hR+59Ky2MftcNS70ULWnWc6I2ZIM0xKcPFyvwU/DpRsTeFm8llayr5bA==",
"dev": true
},
"node_modules/@types/emscripten": {
"version": "1.39.9",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.9.tgz",
"integrity": "sha512-ILdWj4XYtNOqxJaW22NEQx2gJsLfV5ncxYhhGX1a1H1lXl2Ta0gUz7QOnOoF1xQbJwWDjImi8gXN9mKdIf6n9g==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz",
@ -934,6 +947,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/barcode-detector": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.1.0.tgz",
"integrity": "sha512-i9L6Kvz8M7jK+3NHFSxtzUTcHeB6RTztm/dPlfMuz5giRvfp8XKje7yoPU0dIg98WQ8aD7gsbXHg6JzHsrzcaw==",
"dev": true,
"dependencies": {
"@types/dom-webcodecs": "^0.1.9",
"zxing-wasm": "1.0.0-rc.3"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -1637,6 +1660,12 @@
"integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==",
"dev": true
},
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
"dev": true
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -1887,6 +1916,19 @@
}
}
},
"node_modules/vue-qrcode-reader": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.4.0.tgz",
"integrity": "sha512-7ePS41r7Q8BtiPxutFW/N7SeQd5YQe3jdZ1CG/kBGPWYB/RNN0R5MvDV3i4x3aboM8wdvjGFnrN76CXw5HvNew==",
"dev": true,
"dependencies": {
"barcode-detector": "2.1.0",
"webrtc-adapter": "8.2.3"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/vue-router": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz",
@ -1916,6 +1958,28 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
},
"node_modules/webrtc-adapter": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
"dev": true,
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/zxing-wasm": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.0.0-rc.3.tgz",
"integrity": "sha512-rNpPqQ6w/Dym7yK3hMJMMS5DK36J8wCDXYW5FVfW3k83NYT88MTi26AtJE3zIvqOpFr5C0khTjbWfEvJTw4MDA==",
"dev": true,
"dependencies": {
"@types/emscripten": "^1.39.9"
}
}
}
}

View File

@ -27,6 +27,7 @@
"vite": "^4.4.9",
"vue": "^3.3.4",
"vue-draggable-plus": "^0.2.6",
"vue-qrcode-reader": "^5.4.0",
"vue-router": "^4.2.4"
}
}

View File

@ -330,7 +330,7 @@ a:hover {
.fullscreen-streamer {
position: fixed;
top: 10%;
top: 7%;
left: 0;
width: 100%;
height: 65%;
@ -363,7 +363,8 @@ a:hover {
.fullscreen-footer {
position: fixed;
top: calc(100vh - 8rem);
// top: calc(100vh - 8rem);
bottom: 68px;
left: 0;
width: 100%;
text-align: center;

View File

@ -0,0 +1,177 @@
<script setup>
import Form from '@/components/formElements/Form'
import Spinner from '@/components/Spinner.vue'
import { useBusStore } from '@/stores/bus'
import { UseColorMode } from '@vueuse/components'
import { useNotifyStore } from '@/stores/notify'
import { QrcodeStream } from 'vue-qrcode-reader'
const router = useRouter()
const bus = useBusStore()
const notify = useNotifyStore()
const cameraIsOn = ref(false)
const selectedDevice = ref(null)
const devices = ref([])
const errorPhrase = ref('')
const form = reactive(new Form({
qrcode: null,
uri: '',
}))
onMounted(async () => {
devices.value = (await navigator.mediaDevices.enumerateDevices())
.filter(({ kind }) => kind === 'videoinput')
if (devices.value.length > 0) {
selectedDevice.value = devices.value[0]
}
})
const onError = error => {
if (error.name === 'NotAllowedError') {
errorPhrase.value = 'need_grant_permission'
} else if (error.name === 'NotFoundError') {
errorPhrase.value = 'no_cam_on_device'
} else if (error.name === 'NotSupportedError' || error.name === 'InsecureContextError') {
errorPhrase.value = 'secured_context_required'
} else if (error.name === 'NotReadableError') {
errorPhrase.value = 'not_readable'
} else if (error.name === 'OverconstrainedError') {
errorPhrase.value = 'camera_not_suitable'
} else if (error.name === 'StreamApiNotSupportedError') {
errorPhrase.value = 'stream_api_not_supported'
} else {
notify.error(error)
}
}
/**
* Pushes a decoded URI to the Create or Import form
*
* The basicQRcodeReader option is Off, so qrcode decoding has already be done by vue-qrcode-reader, whether
* from livescan or file input.
* We simply check the uri validity to prevent useless push to the form, but the form will check uri validity too.
*/
const onDetect = async (detectedCodes) => {
const [ firstCode ] = detectedCodes
form.uri = firstCode.rawValue
if (! form.uri) {
notify.warn({ text: trans('errors.qrcode_cannot_be_read') })
}
else if (form.uri.slice(0, 33).toLowerCase() == "otpauth-migration://offline?data=") {
bus.migrationUri = form.uri
router.push({ name: 'importAccounts' })
}
else if (form.uri.slice(0, 15).toLowerCase() !== "otpauth://totp/" && form.uri.slice(0, 15).toLowerCase() !== "otpauth://hotp/") {
notify.warn({ text: trans('errors.no_valid_otp') })
}
else {
bus.decodedUri = form.uri
router.push({ name: 'createAccount' })
}
}
/**
* Triggered when camera goes On
*/
function cameraOn(event) {
cameraIsOn.value = true
}
/**
* Triggered when camera goes Off
*/
function cameraOff(event) {
cameraIsOn.value = false
}
/**
* Exits the stream view
*/
function exitStream() {
// this.camera = 'off'
router.go(-1)
}
/**
* Paints the red outline during scan
*/
const paintOutline = (detectedCodes, ctx) => {
for (const detectedCode of detectedCodes) {
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints
ctx.strokeStyle = 'red'
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
for (const { x, y } of otherPoints) {
ctx.lineTo(x, y)
}
ctx.lineTo(firstPoint.x, firstPoint.y)
ctx.closePath()
ctx.stroke()
}
}
</script>
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<section class="section">
<div class="columns is-centered">
<div class="column is-three-quarters">
<div class="modal-slot box has-text-centered is-shadowless">
<div v-if="errorPhrase">
<p class="block is-size-5">{{ $t('twofaccounts.stream.live_scan_cant_start') }}</p>
<UseColorMode v-slot="{ mode }">
<p class="block" :class="{'has-text-light': mode == 'dark'}">{{ $t('twofaccounts.stream.' + errorPhrase + '.reason') }}</p>
</UseColorMode>
<p class="is-size-7">{{ $t('twofaccounts.stream.' + errorPhrase + '.solution') }}</p>
</div>
<UseColorMode v-else v-slot="{ mode }">
<span class="is-size-4" :class="mode == 'dark' ? 'has-text-light':'has-text-grey-dark'">
<Spinner :isVisible="true" :type="'raw'" class="is-size-1" />
</span>
</UseColorMode>
</div>
</div>
</div>
</section>
</div>
<div class="fullscreen-streamer">
<qrcode-stream
v-if="selectedDevice !== null"
:constraints="{ deviceId: selectedDevice.deviceId }"
:track="paintOutline"
@detect="onDetect"
@error="onError"
@camera-on="cameraOn"
@camera-off="cameraOff"
></qrcode-stream>
<!-- device selector -->
<div v-if="cameraIsOn && devices.length > 1" class="field has-addons has-addons-centered mt-3">
<p class="control has-icons-left">
<span class="select">
<select v-model="selectedDevice">
<option v-for="device in devices" :key="device.label" :value="device">
{{ device.label ? device.label : $t('commons.default') }}
</option>
</select>
</span>
<span class="icon is-small is-left">
<FontAwesomeIcon :icon="['fas', 'camera']" />
</span>
</p>
</div>
</div>
<div class="fullscreen-footer">
<!-- Cancel button -->
<button id="btnCancel" class="button is-large is-warning is-rounded" @click="exitStream()">
{{ $t('commons.cancel') }}
</button>
</div>
</div>
</template>

View File

@ -73,4 +73,5 @@
'or' => 'OR',
'close_the_x_page' => 'Close the :pagetitle page',
'submit' => 'Submit',
'default' => 'Default',
];