Files
OSIT-AE-App-Svelte/src/lib/element_qr_scanner_v2.svelte
2025-11-19 13:38:45 -05:00

589 lines
20 KiB
Svelte

<script lang="ts">
import { run, preventDefault } from 'svelte/legacy';
// *** Import Svelte core
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
// import 'html5-qrcode';
import {
Html5Qrcode,
Html5QrcodeScannerState,
Html5QrcodeSupportedFormats
} from 'html5-qrcode';
// *** Import Aether core variables and functions
import { api } from '$lib/api/api';
import { ae_api } from '$lib/stores/ae_stores';
// *** Import Aether core components
// import Element_input from './element_input.svelte';
// import ae from '/element_input.svelte';
// import Input_element from '/element_input.svelte';
// *** Import Aether module variables and functions
// *** Import Aether module components
interface Props {
// *** Export/Exposed variables and functions for component
start_qr_scanner?: boolean;
show_pause_btn?: boolean; // pause and resume buttons
show_qr_manual_text_entry_option?: boolean;
show_qr_manual_badge_id_entry_option?: boolean;
show_qr_scan_result?: boolean;
qr_fps?: number;
qr_viewfinder_width?: number; // 275 seems good... Need to not let the this be larger than the container which changes based on the width of the screen/window.
qr_facing_mode?: string; // environment, user, { exact: 'environment'}, { exact: 'user'}
}
let {
start_qr_scanner = $bindable(true),
show_pause_btn = false,
show_qr_manual_text_entry_option = false,
show_qr_manual_badge_id_entry_option = false,
show_qr_scan_result = true,
qr_fps = 10,
qr_viewfinder_width = 275,
qr_facing_mode = 'environment'
}: Props = $props();
const dispatch = createEventDispatcher();
// *** Set initial variables
let scanning_status: string = $state('not_started');
let qr_scan_result: null | string = $state(null);
let qr_found_text: null | string = null;
let qr_entered_text: null | string = $state(null);
let qr_entered_badge_id: null | string = $state(null);
let show_qr_manual_entry: null | boolean = $state(null);
let disable_submit_badge_id_btn: boolean = $state(true);
let user_media_status = 'not_requested';
let debug_comment: string = 'Debugging QR Scanner';
let debug_info: any;
let html5_qr_code: any | null | string = null;
// let qr_scan_cfg = { fps: 10, qrbox: 400 }; // default was 250 and using 300 when 600px
let qr_scan_cfg = { fps: qr_fps, qrbox: qr_viewfinder_width }; // 275 seems good... Need to not let the this be larger than the container which changes based on the width of the screen/window.
// const html5QrCode = new Html5Qrcode(
// 'qr_scanner_viewfinder', { formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ] }
// );
onMount(() => {
console.log('** Element Mounted: ** QR Scanner');
// html5_qr_code = new Html5Qrcode(
// 'qr_scanner_viewfinder', { formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ] }
// );
// navigator.mediaDevices.getUserMedia({video: true})
// .then(get_user_media_success, get_user_media_error);
// console.log('** Element Mounted: ** QR Scanner - getUserMedia in setTimeout');
});
onDestroy(async () => {
console.log('** Element Destroyed: ** QR Scanner');
qr_scan_result = null;
qr_found_text = null;
await handle_stop_qr_scanning();
});
var get_user_media_success = function (error: any) {
console.log('Camera access allowed');
user_media_status = 'allowed';
debug_comment = 'Camera Access Allowed';
debug_info = JSON.stringify(error);
if (html5_qr_code) {
console.log('html5_qr_code object found. Clearing and creating new Html5Qrcode...');
debug_info = 'html5_qr_code object found. Clearing and creating new Html5Qrcode...';
html5_qr_code.clear();
// document.getElementById('qr_scanner_viewfinder').classList.remove('d_none');
} else {
console.log('html5_qr_code not found. Creating new Html5Qrcode...');
debug_info = 'html5_qr_code not found. Creating new Html5Qrcode...';
html5_qr_code = new Html5Qrcode('qr_scanner_viewfinder', {
formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE]
});
}
// html5_qr_code = new Html5Qrcode(
// 'qr_scanner_viewfinder', { formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE ] }
// );
debug_info = 'new Html5Qrcode for element id=qr_scanner_viewfinder';
if (start_qr_scanner && 1 == 2) {
console.log('Ready to start QR scanning! (after x500ms)');
debug_comment = 'Starting QR after 500ms...';
debug_info = 'Ready to start QR scanning! (after 2500ms)';
setTimeout(() => {
console.log('Inside QR scanning timeout after x500ms...');
handle_start_qr_scanning_trust();
}, 2500);
console.log('Started QR scanning after x500ms...');
}
// let subject = 'Camera Access Allowed';
// let message = error;
// send_init_confirm_email(subject, message);
console.log('Dispatching qr_camera');
debug_info = 'Dispatching qr_camera';
dispatch('qr_camera', {
status: 'allowed'
});
};
var get_user_media_error = function (error: any) {
if (error.name == 'NotAllowedError') {
console.log('Camera access not allowed!');
user_media_status = 'denied';
debug_comment = 'Camera Access Denied';
debug_info = JSON.stringify(error);
// alert('Error trying to start camera');
// alert(error);
let subject = 'Camera Access Denied';
let message = error;
send_init_confirm_email(subject, message);
// dispatch('qr_camera', {
// status: 'denied',
// });
}
};
// $: if (start_qr_scanner && user_media_status == 'allowed' && (scanning_status == 'not_started' || scanning_status == 'paused')) {
// console.log('START QR SCANNING');
// handle_start_qr_scanning();
// } else {
// // console.log('STOP QR SCANNING');
// // handle_stop_qr_scanning();
// }
// $: if (mounted && start_qr_scanner) {
// console.log('START QR SCANNING');
// handle_start_qr_scanning();
// } else if (mounted && !start_qr_scanner) {
// console.log('STOP QR SCANNING');
// handle_stop_qr_scanning();
// }
async function handle_start_qr_scanning_trust() {
console.log('*** handle_start_qr_scanning_trust() ***');
qr_scan_result = null;
qr_found_text = null;
debug_comment = 'Starting trusting QR scanning...';
debug_info = 'Just start!';
await html5_qr_code.clear();
html5_qr_code
.start(
{ facingMode: qr_facing_mode },
qr_scan_cfg,
handle_qr_scan_success,
handle_qr_scan_error
)
.then((ignore: any) => {
console.log('Scanning has started');
scanning_status = 'scanning';
debug_info = 'Scanning has started';
// let subject = 'QR Scanning Started';
// let message = ignore;
// send_init_confirm_email(subject, message);
return true;
})
.catch((err) => {
console.log('There was an error while trying to start the QR scanner');
scanning_status = 'start_error';
debug_info = 'Error starting scanner: ' + JSON.stringify(err);
// let subject = 'QR Scanning Start Error';
// let message = err;
// send_init_confirm_email(subject, message);
// Error getting userMedia, error = NotReadableError: Could not start video source
return false;
});
return true;
}
async function handle_stop_qr_scanning() {
start_qr_scanner = false;
if (!html5_qr_code) {
console.log('html5_qr_code object found. Nothing to stop?');
scanning_status = 'not_started';
return false;
}
await html5_qr_code.stop();
scanning_status = 'not_started';
await html5_qr_code.clear();
return true;
}
// Callback function for QrcodeSuccessCallback (decodedText: string, result: Html5QrcodeResult)
function handle_qr_scan_success(decoded_text, decoded_result) {
console.log(
`*** handle_qr_scan_success() *** QR scanned = ${decoded_text}`,
decoded_result
);
qr_scan_result = decoded_text; // NOTE: decoded_result is not currently used by html5-qrcode
qr_found_text = decoded_text;
dispatch('qr_scan_result', {
result: qr_scan_result, // This text will need to be parsed to get more info.
text: qr_found_text, // This text will need to be parsed to get more info.
entry_method: 'QR'
});
// handle_pause_qr_scanning();
handle_stop_qr_scanning();
}
// Callback function for QrcodeErrorCallback (errorMessage: string, error: Html5QrcodeError)
// NOTE: Most of the time this is normal and not an actual error. It just did not find something to scan.
function handle_qr_scan_error(qr_error_message, qr_code_error) {
// console.log('*** handle_qr_scan_error() ***');
if (qr_code_error.type) {
console.log(`Error scanning code = ${qr_error_message}`, qr_code_error);
return;
}
}
run(() => {
if (
qr_entered_badge_id &&
qr_entered_badge_id.length >= 11 &&
qr_entered_badge_id &&
qr_entered_badge_id.length <= 14
) {
disable_submit_badge_id_btn = false;
} else {
disable_submit_badge_id_btn = true;
}
});
function handle_qr_manual_entry() {
console.log('*** handle_qr_manual_entry() ***');
if (qr_entered_text) {
console.log(`QR entered text = ${qr_entered_text}`);
} else if (qr_entered_badge_id) {
console.log(`QR entered badge ID = ${qr_entered_badge_id}`);
qr_entered_text = `OBJ:ot:event_badge,oi:${qr_entered_badge_id}`;
console.log(`Parse to proper QR badge ID = ${qr_entered_text}`);
}
// html5_qr_code.stop().then((ignore) => {
// console.log('Scanning has stopped');
// document.getElementById('qr_scanner_viewfinder').classList.add('d_none');
// }).catch((err) => {
// console.log('There was an error while trying to stop the scanning');
// });
qr_scan_result = qr_entered_text;
dispatch('qr_scan_result', {
result: qr_scan_result,
entry_method: 'manual'
});
qr_scan_result = null;
qr_entered_text = null;
}
function send_init_confirm_email(subject, message) {
console.log(`*** send_init_confirm_email() *** ${subject}`);
let to_email = 'scott.idem+skdev@oneskyit.com';
// let origin_url = encodeURI(`${data.url.origin}`);
let full_subject = `${subject} Aether QR Scanner Debugging`;
let body_html = `
<div>Scott,
<p>This is an automatic debug email from the Aether QR scanner.</p>
</div>
<br>
<div>
Message:<br>
<pre>
${JSON.stringify(message)}
</pre>
</div>
`;
api.send_email({
api_cfg: $ae_api,
from_email: 'noreply+ae_qr_debug@oneskyit.com',
from_name: 'AE QR Debug',
to_email: to_email,
subject: full_subject,
body_html: body_html
});
}
</script>
<section
class="ae_element qr_scanner border-2 border-slate-500/10 space-y-2 flex flex-col gap-1 justify-center items-center min-w-full max-w-full"
class:not_started={scanning_status == 'not_started'}
class:paused={scanning_status == 'paused'}
class:scanning={scanning_status == 'scanning'}
>
<!-- <header>
<h2>QR Scanner</h2>
</header> -->
<!-- <fieldset class=""> -->
<!-- <legend class="d_none">QR Scanner:</legend> -->
<div class="ae_container qr_scanning_container">
<div class="ae_options flex flex-row justify-center items-center gap-1 m-1">
<button
type="button"
onclick={() => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then(get_user_media_success, get_user_media_error);
}}
class="ae_btn__allow_camera btn btn-sm preset-tonal-primary"
>
<span class="fas fa-camera mx-1"></span>
Allow Camera Access
</button>
<button
type="button"
onclick={() => {
handle_start_qr_scanning_trust();
// Select back camera or fail with `OverconstrainedError`.
// html5_qr_code.start({ facingMode: { exact: "environment"} }, config, qrCodeSuccessCallback);
}}
class="ae_btn__start btn btn-sm preset-tonal-primary"
>
<span class="fas fa-qrcode mx-1"></span>
Start Scanning
</button>
<!-- <button
on:click={ () => {
html5_qr_code.start({ facingMode: { exact: "environment"} }, config, qrCodeSuccessCallback);
}}
class="ae_btn__resume btn btn-sm variant-soft-primary">
<span class="fas fa-play"></span>
Resume
</button> -->
{#if scanning_status == 'scanning'}
<button
onclick={handle_stop_qr_scanning}
class="ae_btn__stop btn btn-sm preset-tonal-secondary"
>
<span class="fas fa-crosshairs fa-spin opacity-50 m-1"></span>
<!-- <span class="fas fa-stop-circle m-1"></span> -->
Stop
</button>
{/if}
</div>
<div
id="qr_scanner_viewfinder"
class="qr_scanner_viewfinder grow flex flex-col justify-center items-center"
style=""
></div>
<!-- width: 600px -->
</div>
{#if show_qr_manual_text_entry_option}
<div class="ae_container qr_manual_entry text_entry">
{#if show_qr_manual_entry}
<label for="entered_text" class="">Enter text</label>
<input
type="text"
name="entered_text"
id="entered_text"
bind:value={qr_entered_text}
/>
<button onclick={handle_qr_manual_entry} class="btn btn-md preset-tonal-warning"
><span class="fas fa-paper-plane"></span> Submit Text</button
>
<div class="search_by_text">
<input
type="text"
placeholder="Name or Email"
label="Name or Email"
value={search_query_str}
focus={true}
ononinput={handle_oninput_search_query_str}
/>
</div>
{:else}
<button
onclick={() => {
handle_stop_qr_scanning();
show_qr_manual_entry = true;
}}
class="btn btn-md preset-tonal-warning m-1"
>
<span class="fas fa-keyboard mx-1"></span> Enter Text
</button>
{/if}
</div>
{/if}
{#if show_qr_manual_badge_id_entry_option}
<div class="ae_container qr_manual_entry badge_id_entry">
{#if show_qr_manual_entry}
<form onsubmit={preventDefault(() => handle_qr_manual_entry)} class="flex">
<!-- <label for="entered_badge_id" class="">Enter badge ID</label>
<input type="text" name="entered_badge_id" id="entered_badge_id" bind:value="{qr_entered_badge_id}"> -->
<input
bind:value={qr_entered_badge_id}
type="text"
name="entered_badge_id"
id="entered_badge_id"
required
placeholder="Enter Badge ID"
class="input max-w-52"
/>
<button
type="submit"
onclick={handle_qr_manual_entry}
disabled={disable_submit_badge_id_btn}
class="btn btn-md preset-tonal-primary border border-primary-500 m-1"
class:btn_default={disable_submit_badge_id_btn}
class:btn_primary={!disable_submit_badge_id_btn}
>
<span class="fas fa-paper-plane mx-1"></span> Submit Badge ID
</button>
</form>
{:else}
<button
onclick={() => {
handle_stop_qr_scanning();
show_qr_manual_entry = true;
}}
class="btn btn-md preset-tonal-secondary m-1"
>
<span class="fas fa-keyboard mx-1"></span> Enter Badge ID
</button>
{/if}
</div>
{/if}
{#if show_qr_scan_result && qr_scan_result}
<div class="ae_container qr_scan_result">
<span class="label">Raw Result:</span>
<span id="qr_scan_result_value" class="value">{qr_scan_result}</span>
</div>
{/if}
<!-- {debug_comment ?? 'Debugging QR Scanner'}
{#if debug_info}
<div class="ae_container debug_info">
<span class="label">Debug Info:</span>
<span class="value">{debug_info}</span>
</div>
{/if} -->
<!-- </fieldset> -->
<p>
v2 - Try pressing the "Allow Camera Access" button and then the "Start Scanning" button if
it does not start on its own. This fix is not perfect. A permanent solution is actively
being worked on in the development version.
</p>
</section>
<style>
.not_started {
background-color: hsla(0, 100%, 75%, 0.3);
border-color: hsla(0, 100%, 75%, 0.6);
}
.paused {
background-color: hsla(60, 100%, 75%, 0.3);
border-color: hsla(60, 100%, 75%, 0.6);
}
.scanning {
background-color: hsla(120, 100%, 75%, 0.3);
border-color: hsla(120, 100%, 75%, 0.6);
}
.qr_scanner {
/* outline: solid thin pink; */
max-width: 100vw;
/* overflow-x: scroll; */
display: flex;
flex-direction: column;
/* flex-wrap: wrap; */
justify-content: flex-start;
align-items: center; /* center */
align-content: stretch;
}
.ae_element.qr_scanner div.qr_scanner_viewfinder {
/* max-width: 100vw; */
/* contain: content; */
/* contain: contain; */
}
.qr_scanner .qr_scanner_viewfinder {
/* outline: dashed medium blue; */
min-width: 400px;
width: 100%;
/* max-width: 100%; */
max-width: 500px;
/* max-width: 100vw; */
/* outline: solid thin red; */
contain: contain;
overflow-x: scroll;
}
@media (max-width: 767px) {
.qr_scanner .qr_scanner_viewfinder {
/* outline: dashed medium red; */
min-width: 80vw;
/* width: 100%; */
/* max-width: 100%; */
/* max-width: 450px; */
max-width: 100vw;
margin: 0;
padding: 0;
}
}
</style>