-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/qr barcode reader #8153
Changes from 4 commits
5836c2b
644d0f2
e060db2
4b5fa6a
ad13bbb
91e63ca
806d832
dc38ad0
cbdf7c8
6809bd7
a39f6af
c162ad4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3157,6 +3157,57 @@ | |
} | ||
] | ||
}, | ||
"codescanner": { | ||
"name": "Code Scanner", | ||
"icon": "Camera", | ||
"styles": [ | ||
"size" | ||
], | ||
"draggable": true, | ||
"illegalChildren": [ | ||
"section" | ||
], | ||
"settings": [ | ||
{ | ||
"type": "field/scannedcode", | ||
"label": "Field", | ||
"key": "field", | ||
"required": true | ||
}, | ||
{ | ||
"type": "text", | ||
"label": "Label", | ||
"key": "label" | ||
}, | ||
{ | ||
"type": "text", | ||
"label": "Button", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: let's rename this to "Button text" to make it more obvious that's what it is |
||
"key": "scanButtonText" | ||
}, | ||
{ | ||
"type": "text", | ||
"label": "Default value", | ||
"key": "defaultValue" | ||
}, | ||
{ | ||
"type": "boolean", | ||
"label": "Disabled", | ||
"key": "disabled", | ||
"defaultValue": false | ||
}, | ||
{ | ||
"type": "boolean", | ||
"label": "Allow manual entry", | ||
"key": "allowManualEntry", | ||
"defaultValue": false | ||
}, | ||
{ | ||
"type": "validation/string", | ||
"label": "Validation", | ||
"key": "validation" | ||
} | ||
] | ||
}, | ||
"embeddedmap": { | ||
"name": "Embedded Map", | ||
"icon": "Location", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
<script> | ||
import { ModalContent, Modal, Icon, ActionButton } from "@budibase/bbui" | ||
import { Input, Button, StatusLight } from "@budibase/bbui" | ||
import { Html5Qrcode } from "html5-qrcode" | ||
|
||
export let value | ||
export let disabled = false | ||
export let allowManualEntry = false | ||
export let scanButtonText = "Scan Code" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: Change casing to "Scan code" for consistency with other buttons |
||
|
||
import { createEventDispatcher } from "svelte" | ||
const dispatch = createEventDispatcher() | ||
|
||
let videoEle | ||
let camModal | ||
let manualMode = false | ||
let cameraEnabled | ||
let cameraStarted = false | ||
let html5QrCode | ||
let cameraSetting = { facingMode: "environment" } | ||
let cameraConfig = { | ||
fps: 25, | ||
qrbox: { width: 250, height: 250 }, | ||
} | ||
const onScanSuccess = decodedText => { | ||
if (value != decodedText) { | ||
dispatch("change", decodedText) | ||
} | ||
} | ||
|
||
const initReader = async () => { | ||
if (html5QrCode) { | ||
html5QrCode.stop() | ||
} | ||
html5QrCode = new Html5Qrcode("reader") | ||
return new Promise(resolve => { | ||
html5QrCode | ||
.start(cameraSetting, cameraConfig, onScanSuccess) | ||
.then(() => { | ||
resolve({ initialised: true }) | ||
}) | ||
.catch(err => { | ||
console.log("There was a problem scanning the image", err) | ||
resolve({ initialised: false }) | ||
}) | ||
}) | ||
} | ||
|
||
const checkCamera = async () => { | ||
return new Promise(resolve => { | ||
Html5Qrcode.getCameras() | ||
.then(devices => { | ||
if (devices && devices.length) { | ||
resolve({ enabled: true }) | ||
} | ||
}) | ||
.catch(e => { | ||
console.error(e) | ||
resolve({ enabled: false }) | ||
}) | ||
}) | ||
} | ||
|
||
const start = async () => { | ||
const status = await initReader() | ||
cameraStarted = status.initialised | ||
} | ||
|
||
$: if (cameraEnabled && videoEle && !cameraStarted) { | ||
start() | ||
} | ||
|
||
const showReaderModal = async () => { | ||
camModal.show() | ||
const camStatus = await checkCamera() | ||
cameraEnabled = camStatus.enabled | ||
} | ||
|
||
const hideReaderModal = async () => { | ||
cameraEnabled = undefined | ||
cameraStarted = false | ||
if (html5QrCode) { | ||
await html5QrCode.stop() | ||
html5QrCode = undefined | ||
} | ||
camModal.hide() | ||
} | ||
</script> | ||
|
||
<div class="scanner-video-wrapper"> | ||
{#if value && !manualMode} | ||
<div class="scanner-value field-display"> | ||
<StatusLight positive /> | ||
{value} | ||
</div> | ||
{/if} | ||
|
||
{#if allowManualEntry && manualMode} | ||
<div class="manual-input"> | ||
<Input | ||
bind:value | ||
on:change={() => { | ||
dispatch("change", value) | ||
}} | ||
/> | ||
</div> | ||
{/if} | ||
|
||
{#if value} | ||
<ActionButton | ||
on:click={() => { | ||
dispatch("change", "") | ||
}} | ||
{disabled} | ||
> | ||
Clear | ||
</ActionButton> | ||
{:else} | ||
<ActionButton | ||
icon="Camera" | ||
on:click={() => { | ||
showReaderModal() | ||
}} | ||
{disabled} | ||
> | ||
{scanButtonText} | ||
</ActionButton> | ||
{/if} | ||
</div> | ||
|
||
<div class="modal-wrap"> | ||
<Modal bind:this={camModal} on:hide={hideReaderModal}> | ||
<ModalContent | ||
title={scanButtonText} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
2 ways to solve this would be: |
||
showConfirmButton={false} | ||
showCancelButton={false} | ||
> | ||
<div id="reader" class="container" bind:this={videoEle}> | ||
<div class="camera-placeholder"> | ||
<Icon size="XXL" name="Camera" /> | ||
{#if cameraEnabled === false} | ||
<div>Your camera is disabled.</div> | ||
{/if} | ||
</div> | ||
</div> | ||
{#if cameraEnabled === true} | ||
<div class="code-wrap"> | ||
{#if value} | ||
<div class="scanner-value"> | ||
<StatusLight positive /> | ||
{value} | ||
</div> | ||
{:else} | ||
<div class="scanner-value"> | ||
<StatusLight neutral /> | ||
Searching for code... | ||
</div> | ||
{/if} | ||
</div> | ||
{/if} | ||
|
||
<div slot="footer"> | ||
<div class="footer-buttons"> | ||
{#if allowManualEntry} | ||
<Button | ||
group | ||
secondary | ||
newStyles | ||
on:click={() => { | ||
manualMode = !manualMode | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: since this is just a toggle, if you've enabled manual mode already and then press this again, it will unset it which feels a bit strange. I reckon we should probably just hide the button if we're already in manual mode, so |
||
camModal.hide() | ||
}} | ||
> | ||
Enter Manually | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: change casing to "Enter manually" to be consistent with other buttons. |
||
</Button> | ||
{/if} | ||
|
||
<Button | ||
group | ||
cta | ||
on:click={() => { | ||
camModal.hide() | ||
}} | ||
> | ||
Confirm | ||
</Button> | ||
</div> | ||
</div> | ||
</ModalContent> | ||
</Modal> | ||
</div> | ||
|
||
<style> | ||
#reader :global(video) { | ||
border-radius: 4px; | ||
border: var(--border-light-2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a hardcoded light border so it looks off in dark themes. Let's use border: 2px solid var(--spectrum-global-color-gray-300); instead so that it looks better in any theme. |
||
overflow: hidden; | ||
} | ||
.field-display :global(.spectrum-Tags-item) { | ||
margin: 0px; | ||
} | ||
.footer-buttons { | ||
display: flex; | ||
grid-area: buttonGroup; | ||
gap: var(--spectrum-global-dimension-static-size-200); | ||
} | ||
.scanner-value { | ||
display: flex; | ||
} | ||
.field-display { | ||
padding-top: var( | ||
--spectrum-fieldlabel-side-m-padding-top, | ||
var(--spectrum-global-dimension-size-100) | ||
); | ||
margin-bottom: var(--spacing-m); | ||
} | ||
.camera-placeholder { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
border-radius: 4px; | ||
border: var(--border-light-2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a hardcoded light border so it looks off in dark themes. Let's use border: 2px solid var(--spectrum-global-color-gray-300); instead so that it looks better in any theme. |
||
background-color: var(--spectrum-global-color-gray-200); | ||
flex-direction: column; | ||
gap: var(--spectrum-global-dimension-static-size-200); | ||
} | ||
.container, | ||
.camera-placeholder { | ||
width: 100%; | ||
min-height: 240px; | ||
} | ||
.manual-input { | ||
padding-bottom: var(--spacing-m); | ||
} | ||
</style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB: setting draggable to true here is redundant as it's only a flag used to disable DND if set to false. Setting illegal children is also redundant since this component doesn't take child components anyway.