btcpayserver/BTCPayServer/Views/Shared/CameraScanner.cshtml
d11n ed7031981b
Bootstrap v5 migration (#2490)
* Swap bootstrap asset files

* Update themes and color definitions

* Move general bootstrap customizations

* Theme updates

Theme updates

* Remove BuildBundlerMinifier

This lead to an error, because BuildBundlerMinifier and BundlerMinifier.Core seem to conflict here. Details: https://stackoverflow.com/a/61119586

* Rewplace btn-block class with w-100

* Update badge classes

* Remove old font family head variable

* Update margin classes

* Cleanups

* Update float classes

* Update text classes

* Update padding classes

* Update border classes

* UPdate dropdown classes

* Update select classes

* Update neutral custom props

* Update bootstrap and customizations

* Update ChromeDriver; disable smooth scroll

https://github.com/SeleniumHQ/selenium/issues/8295

* Improve alert messages

* Improve bootstrap customizations

* Disable reduced motion

See also 7358282f

* Update Bootstrap data attributes

* Update file inputs

* Update input groups

* Replace deprecated jumbotron class

* Update variables; re-add negative margin util classes

* Update cards

* Update form labels

* Debug alerts

* Fix aria-labelledby associations

* Dropdown-related test fixes

* Fix CanUseWebhooks test

* Test fixes

* Nav updates

* Fix nav usage in wallet send and payouts

* Update alert and modal close buttons

* Re-add backdrop properties

* Upgrade Bootstrap to v5 final

* Update screen reader classes

* Update font-weight classes

* Update monospace font classes

* Update accordians

* Update close icon usage

* Cleanup

* Update scripts and style integrations

* Update input group texts

* Update LN node setup page

* Update more form control classes

* Update inline forms

* Add js specific test

* Upgrade Vue.js

* Remove unused JS

* Upgrade Bootstrap to v5.0.1

* Try container related test updates

* Separate jQuery bundle

* Remove jQuery from LND seed backup page

* Remove unused code

* Refactor email autofill js

* Refactor camera scanner JS

* Re-add tests

* Re-add BuildBundlerMinifier

* Do not minify bundles containing Bootstrap

Details https://github.com/madskristensen/BundlerMinifier/issues/558

* Update bundles

* Cleanup JS test

* Cleanup tests involving dropdowns

* Cleanup tests involving collapses

* Cleanup locale additions in ConfigureCore

* Cleanup bundles

* Remove duplicate status message

* Cleanup formatting

* Fix missing validation scripts

* Remove unused unminified Bootstrap js files

* Fix classic theme

* Fix Casa theme

* Fix PoS validation
2021-05-19 11:39:27 +09:00

185 lines
5.6 KiB
Text

<template id="camera-qr-scanner-wrap">
<div v-if="modalId" :id="modalId" class="modal fade" data-bs-backdrop="static">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{title}}
<span v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</span>
</h5>
<button type="button" class="btn-close" aria-label="Close" v-on:click="close">
<vc:icon symbol="close"/>
</button>
</div>
<div class="modal-body">
<slot/>
</div>
</div>
</div>
</div>
<div v-else>
<slot></slot>
<div v-if="workload.length > 0">Animated QR detected: {{workload.length}} / {{workload[0].total}} scanned</div>
</div>
</template>
<div id="camera-qr-scanner-modal-app" v-cloak class="only-for-js">
<scanner-wrap v-bind="$data" v-on:close="close">
<div v-if="isLoaded && requestInput" class="d-flex justify-content-center align-items-center" :class="{'border border-secondary': !isModal}">
<div class="spinner-border text-secondary position-absolute" role="status"></div>
<qrcode-drop-zone v-on:decode="onDecode" v-on:init="logErrors">
<qrcode-stream v-on:decode="onDecode" v-on:init="onInit" :camera="camera" :track="paint"/>
</qrcode-drop-zone>
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="camera"/>
</div>
<div v-else-if="qrData || errorMessage">
<div v-if="errorMessage" class="alert alert-danger" role="alert">
{{errorMessage}}
</div>
<div class="text-break font-monospace">
{{qrData}}
</div>
<div class="mt-4">
<button type="button" class="btn btn-primary me-1" v-if="qrData" v-on:click="submitData">Submit</button>
<button type="button" class="btn btn-secondary me-1" v-on:click="retry">Retry</button>
<button type="button" class="btn btn-outline-secondary" v-if="isModal" v-on:click="close">Cancel</button>
</div>
</div>
</scanner-wrap>
</div>
<script>
function initCameraScanningApp(title, onDataSubmit, modalId) {
const isModal = !!modalId;
Vue.component('scanner-wrap', {
props: ["modalId", "title", "workload"],
template: "#camera-qr-scanner-wrap",
methods: {
close() {
this.$emit('close');
}
}
});
new Vue({
el: '#camera-qr-scanner-modal-app',
data() {
return {
isModal,
isLoaded: !isModal,
title: title,
modalId: modalId,
noStreamApiSupport: false,
qrData: null,
errorMessage: null,
workload: [],
camera: "auto"
}
},
mounted() {
if (this.isModal) {
const modal = document.getElementById(this.modalId);
modal.addEventListener('shown.bs.modal', () => { this.isLoaded = true; });
modal.addEventListener('hide.bs.modal', () => { this.isLoaded = false; });
} else {
this.isLoaded = true;
}
},
computed: {
requestInput() {
return this.camera === 'auto' && this.errorMessage === null;
}
},
methods: {
setQrData (qrData) {
this.qrData = qrData;
this.camera = qrData ? "off" : "auto";
},
retry() {
this.camera = "off";
this.$nextTick(this.reset);
},
reset() {
this.setQrData(null);
this.errorMessage = null;
this.workload = [];
},
close() {
if (this.modalId) {
const modal = bootstrap.Modal.getInstance(document.getElementById(this.modalId));
modal.hide();
}
this.reset();
},
onDecode(content) {
if (this.qrData) return;
if (!content.toLowerCase().startsWith("ur:")) {
this.setQrData(content);
this.workload = [];
} else {
const [index, total] = window.bcur.extractSingleWorkload(content);
if (this.workload.length > 0) {
const currentTotal = this.workload[0].total;
if (total !== currentTotal) {
this.workload = [];
}
}
if (!this.workload.find(i => i.index === index)) {
this.workload.push({
index,
total,
data: content,
});
if (this.workload.length === total) {
const decoded = window.bcur.decodeUR(this.workload.map(i => i.data));
this.setQrData(decoded);
}
}
}
},
submitData() {
if (onDataSubmit) {
onDataSubmit(this.qrData);
}
this.close();
},
logErrors(promise) {
promise.catch(console.error)
},
paint(location, ctx) {
ctx.fillStyle = '#137547';
[
location.topLeftFinderPattern,
location.topRightFinderPattern,
location.bottomLeftFinderPattern
].forEach(({ x, y }) => {
ctx.fillRect(x - 5, y - 5, 10, 10);
})
},
onInit(promise) {
promise.then(() => {
this.errorMessage = null;
}).catch(error => {
if (error.name === 'StreamApiNotSupportedError') {
this.noStreamApiSupport = true;
} else if (error.name === 'NotAllowedError') {
this.errorMessage = 'A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.'
} else if (error.name === 'NotFoundError') {
this.errorMessage = 'A camera was not detected on your device.'
} else if (error.name === 'NotSupportedError') {
this.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
} else if (error.name === 'NotReadableError') {
this.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
} else if (error.name === 'OverconstrainedError') {
this.errorMessage = 'Constraints don\'t match any installed camera.'
} else {
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
}
})
}
}
});
}
</script>