aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/scan
diff options
context:
space:
mode:
authorJurjen de Vries <[email protected]>2025-09-11 00:48:15 +0200
committerGitHub <[email protected]>2025-09-11 00:48:15 +0200
commit0c69c9848c6b98c92609d209103efc60003c9723 (patch)
tree445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/routes/(app)/scan
parent2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff)
parent9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff)
downloadcoinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz
coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/routes/(app)/scan')
-rw-r--r--src/routes/(app)/scan/+page.svelte223
1 files changed, 31 insertions, 192 deletions
diff --git a/src/routes/(app)/scan/+page.svelte b/src/routes/(app)/scan/+page.svelte
index 88a76fbb..b21a7458 100644
--- a/src/routes/(app)/scan/+page.svelte
+++ b/src/routes/(app)/scan/+page.svelte
@@ -1,222 +1,61 @@
<script>
+ import { tick } from "svelte";
+ import Icon from "$comp/Icon.svelte";
+ import QrScanner from "qr-scanner";
+ import { onMount, onDestroy } from "svelte";
import { back } from "$lib/utils";
import { goto } from "$app/navigation";
- import { onMount, onDestroy } from "svelte";
import { BarcodeDetector } from "barcode-detector/ponyfill";
- import { camera } from "$lib/store";
+
+ let scanner,
+ vid = $state(),
+ resizing;
let resize = () => {
if (resizing) clearTimeout(resizing);
resizing = setTimeout(initialize, 1000);
};
- let videoEl;
- let canvasEl;
- let barcodeDetector;
- let scanningFrameId;
- let cameras = [];
- let detectionResult = null; // will hold the detected QR code value
-
- // New variables to handle iOS behavior.
- let isIOS = false;
- let scanningStarted = false;
-
- // Enumerate available video input devices
- async function getCameraDevices() {
- try {
- const devices = await navigator.mediaDevices.enumerateDevices();
- cameras = devices.filter((device) => device.kind === "videoinput");
-
- // Simply choose the first available camera.
- if (!$camera && cameras.length > 0) {
- $camera = cameras[0].deviceId;
- }
- } catch (err) {
- console.error("Error enumerating devices:", err);
- }
- }
-
- // Start the camera stream for a given deviceId (used for non-iOS or programmatic restarts)
- async function startCamera(deviceId) {
- stopCameraStream();
- detectionResult = null; // reset detection on restart
- try {
- const constraints = deviceId
- ? { video: { deviceId: { exact: deviceId } } }
- : { video: { facingMode: "environment" } };
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
- videoEl.srcObject = stream;
- await videoEl.play();
- } catch (err) {
- console.error("Error accessing camera:", err);
- }
- }
-
- // Stop any active video stream and cancel the scanning loop
- function stopCameraStream() {
- if (videoEl && videoEl.srcObject) {
- const stream = videoEl.srcObject;
- if (stream.getTracks) {
- stream.getTracks().forEach((track) => track.stop());
- }
- }
- if (scanningFrameId) {
- cancelAnimationFrame(scanningFrameId);
- }
- }
-
- function handleDetection(barcode) {
- detectionResult = barcode.rawValue;
- stopCameraStream();
- goto(`/send/${encodeURIComponent(detectionResult)}`);
- }
-
- // The scanning loop: draw the video frame onto a hidden canvas and detect QR codes.
- function scanLoop() {
- if (!videoEl.videoWidth || !videoEl.videoHeight) {
- scanningFrameId = requestAnimationFrame(scanLoop);
- return;
- }
-
- const context = canvasEl.getContext("2d");
- if (!context) return;
-
- // Ensure canvas dimensions match the video
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
-
- barcodeDetector
- .detect(canvasEl)
- .then((barcodes) => {
- if (barcodes.length > 0) {
- handleDetection(barcodes[0]);
- return; // Stop scanning on detection.
- }
- })
- .catch((err) => {
- console.error("Detection error:", err);
- });
-
- // Continue scanning if no detection has occurred.
- if (!detectionResult) {
- scanningFrameId = requestAnimationFrame(scanLoop);
- }
- }
-
- // Handle change of camera selection
- async function onCameraChange(event) {
- $camera = event.target.value;
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
- }
-
- // Provide a way to restart scanning (non-iOS)
- async function restart() {
- detectionResult = null;
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
- }
-
- // For iOS: Use a direct promise chain to call getUserMedia as part of the user gesture.
- function startScanning() {
- scanningStarted = true;
- stopCameraStream();
- detectionResult = null;
- const constraints = $camera
- ? { video: { deviceId: { exact: $camera } } }
- : { video: { facingMode: "environment" } };
- navigator.mediaDevices.getUserMedia(constraints)
- .then((stream) => {
- videoEl.srcObject = stream;
- return videoEl.play();
- })
- .then(() => {
- // When video is ready, start scanning.
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- })
- .catch((err) => {
- console.error("Error accessing camera:", err);
- });
- }
-
let initialize = async () => {
- // Detect if we're on an iOS device
- isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
- barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
- await getCameraDevices();
-
- // For non-iOS devices, auto-start scanning.
- if (!isIOS) {
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
+ if (scanner) {
+ scanner.stop();
+ await new Promise((r) => setTimeout(r, 1000));
}
+
+ let options = {
+ highlightScanRegion: true,
+ highlightCodeOutline: true,
+ qrEngine: BarcodeDetector,
+ };
+ let cb = ({ data }) =>
+ scanner.stop() || goto(`/send/${encodeURIComponent(data)}`);
+ scanner = new QrScanner(vid, cb, options);
+ await tick();
+ scanner.start();
};
onMount(initialize);
-
- onDestroy(() => {
- stopCameraStream();
- });
+ onDestroy(() => scanner?.stop());
</script>
<svelte:window onresize={resize} />
+
<div class="flex w-full mb-4 p-4">
- <div class="mx-auto rounded-3xl space-y-5">
+ <div class="mx-auto rounded-3xl">
<video
- bind:this={videoEl}
+ bind:this={vid}
class="border-4 rounded-3xl border-black max-h-[calc(100vh*0.7)]"
></video>
- <canvas bind:this={canvasEl} style="display: none;"></canvas>
- <div class="flex justify-center">
- <button class="btn !w-auto" onclick={back}>Cancel</button>
- </div>
- <!-- Only display the camera selection if there are multiple cameras -->
- <div>
- <select
- id="cameraSelect"
- bind:value={$camera}
- onchange={onCameraChange}
- >
- {#each cameras as camera}
- <option value={camera.deviceId}>
- {camera.label || `Camera ${camera.deviceId}`}
- </option>
- {/each}
- </select>
- </div>
- <!-- For iOS users, show a "Start Scanning" button if scanning hasn't begun -->
- {#if isIOS && !scanningStarted}
- <div class="flex justify-center">
- <button class="btn !w-auto" onclick={startScanning}>
- Start Scanning
- </button>
- </div>
- {/if}
</div>
</div>
+<div class="flex justify-center">
+ <button class="btn !w-auto" onclick={back}>Cancel</button>
+</div>
+
<style>
video {
- width: 100%;
- max-width: 600px;
- border: 4px solid black;
- border-radius: 1rem;
- transform: scale(1);
transition: transform 0.3s ease-out;
+ transform: scale(1);
}
</style>