mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-22 16:23:18 +01:00
Set up the Capture view
This commit is contained in:
parent
b516fd9c33
commit
7cf8a70743
64
package-lock.json
generated
64
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
5
resources/js_vue3/assets/app.scss
vendored
5
resources/js_vue3/assets/app.scss
vendored
@ -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;
|
||||
|
177
resources/js_vue3/views/twofaccounts/Capture.vue
Normal file
177
resources/js_vue3/views/twofaccounts/Capture.vue
Normal 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>
|
@ -73,4 +73,5 @@
|
||||
'or' => 'OR',
|
||||
'close_the_x_page' => 'Close the :pagetitle page',
|
||||
'submit' => 'Submit',
|
||||
'default' => 'Default',
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user