Skip to content

Commit 5d2d069

Browse files
authored
Merge pull request #2 from thedivtagguy/qr-code
2 parents c8ae07e + dab6f36 commit 5d2d069

7 files changed

Lines changed: 1071 additions & 163 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"valibot": "^0.42.1",
7878
"vaul-svelte": "1.0.0-next.7",
7979
"vite": "^6.2.5",
80+
"vite-plugin-mkcert": "^1.17.9",
8081
"vite-plugin-wasm-module-workers": "^0.0.2",
8182
"yup": "^1.6.1",
8283
"zod": "^3.25.56"
@@ -92,6 +93,7 @@
9293
"d3-hierarchy": "^3.1.2",
9394
"d3-sankey": "^0.12.3",
9495
"flexsearch": "^0.8.158",
96+
"html5-qrcode": "^2.3.8",
9597
"instantsearch.js": "^4.78.3",
9698
"pack-spheres": "^1.0.4",
9799
"satori": "^0.18.2",

pnpm-lock.yaml

Lines changed: 60 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/actions/qrScanner.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { Html5Qrcode, type Html5QrcodeResult } from 'html5-qrcode';
2+
import type { Action } from 'svelte/action';
3+
import {
4+
selectBackCamera,
5+
isCameraSupported,
6+
getCameraErrorMessage,
7+
type QRScannerConfig,
8+
DEFAULT_SCANNER_CONFIG
9+
} from '$lib/utils/qrScanner';
10+
11+
export interface QRScannerOptions {
12+
config?: QRScannerConfig;
13+
autoStart?: boolean;
14+
isActive?: boolean;
15+
onScanSuccess: (decodedText: string, result: Html5QrcodeResult) => void;
16+
onScanFailure?: (error: string) => void;
17+
onStateChange?: (state: ScannerState) => void;
18+
onError?: (error: string) => void;
19+
}
20+
21+
export type ScannerState = 'idle' | 'initializing' | 'scanning' | 'stopping' | 'error';
22+
23+
/**
24+
* Custom Svelte action for QR code scanning
25+
*
26+
* Usage:
27+
* ```svelte
28+
* <div use:qrScanner={{
29+
* config: { fps: 10 },
30+
* onScanSuccess: handleScan,
31+
* onStateChange: (state) => scannerState = state
32+
* }}></div>
33+
* ```
34+
*/
35+
export const qrScanner: Action<HTMLDivElement, QRScannerOptions> = (node, options) => {
36+
let scanner: Html5Qrcode | null = null;
37+
let currentState: ScannerState = 'idle';
38+
39+
const updateState = (newState: ScannerState) => {
40+
currentState = newState;
41+
options?.onStateChange?.(newState);
42+
};
43+
44+
const handleScanFailure = (error: string) => {
45+
// Silently ignore decode errors - they happen constantly while scanning
46+
options?.onScanFailure?.(error);
47+
};
48+
49+
async function start() {
50+
console.log('[QR Scanner Action] start() called, currentState:', currentState);
51+
if (currentState !== 'idle' && currentState !== 'error') {
52+
console.log('[QR Scanner Action] Not idle, skipping start');
53+
return;
54+
}
55+
56+
updateState('initializing');
57+
58+
try {
59+
// Check for camera support
60+
if (!isCameraSupported()) {
61+
throw new Error('Camera not supported. Please use HTTPS or a modern browser.');
62+
}
63+
64+
// Ensure the node has an ID BEFORE initializing scanner
65+
if (!node.id) {
66+
node.id = `qr-scanner-${Math.random().toString(36).substr(2, 9)}`;
67+
}
68+
console.log('[QR Scanner Action] Node ID:', node.id);
69+
70+
// Get camera ID
71+
const cameraId = await selectBackCamera();
72+
console.log('[QR Scanner Action] Camera ID:', cameraId);
73+
74+
// Initialize scanner
75+
if (!scanner) {
76+
console.log('[QR Scanner Action] Creating new Html5Qrcode instance');
77+
scanner = new Html5Qrcode(node.id, {
78+
verbose: false
79+
});
80+
}
81+
82+
// Start scanning
83+
console.log('[QR Scanner Action] Starting scanner...');
84+
await scanner.start(
85+
cameraId,
86+
{ ...DEFAULT_SCANNER_CONFIG, ...options.config },
87+
options.onScanSuccess,
88+
handleScanFailure
89+
);
90+
91+
console.log('[QR Scanner Action] Scanner started successfully');
92+
updateState('scanning');
93+
} catch (err: any) {
94+
console.error('[QR Scanner Action] Failed to start scanner:', err);
95+
const errorMessage = getCameraErrorMessage(err);
96+
options?.onError?.(errorMessage);
97+
updateState('error');
98+
}
99+
}
100+
101+
async function stop() {
102+
if (currentState !== 'scanning' || !scanner) {
103+
return;
104+
}
105+
106+
updateState('stopping');
107+
108+
try {
109+
await scanner.stop();
110+
updateState('idle');
111+
} catch (err) {
112+
console.error('Failed to stop scanner:', err);
113+
updateState('idle');
114+
}
115+
}
116+
117+
async function cleanup() {
118+
if (scanner) {
119+
try {
120+
if (currentState === 'scanning') {
121+
await scanner.stop();
122+
}
123+
scanner.clear();
124+
} catch (err) {
125+
console.error('Cleanup error:', err);
126+
}
127+
}
128+
}
129+
130+
// Auto-start if enabled (unless isActive is explicitly provided)
131+
if (options.isActive === undefined) {
132+
// No isActive control, auto-start by default
133+
if (options.autoStart !== false) {
134+
start();
135+
}
136+
} else if (options.isActive) {
137+
// isActive is true, start immediately
138+
start();
139+
}
140+
141+
return {
142+
update(newOptions: QRScannerOptions) {
143+
const wasActive = options.isActive;
144+
options = newOptions;
145+
146+
// Handle isActive changes
147+
if (newOptions.isActive !== undefined && newOptions.isActive !== wasActive) {
148+
if (newOptions.isActive) {
149+
start();
150+
} else {
151+
stop();
152+
}
153+
}
154+
},
155+
destroy() {
156+
cleanup();
157+
}
158+
};
159+
};
160+
161+
/**
162+
* Creates a controller for programmatically controlling the scanner
163+
*/
164+
export function createScannerController() {
165+
let startFn: (() => Promise<void>) | null = null;
166+
let stopFn: (() => Promise<void>) | null = null;
167+
168+
return {
169+
setCallbacks(start: () => Promise<void>, stop: () => Promise<void>) {
170+
startFn = start;
171+
stopFn = stop;
172+
},
173+
async start() {
174+
await startFn?.();
175+
},
176+
async stop() {
177+
await stopFn?.();
178+
}
179+
};
180+
}

src/lib/assets/demo.jpg

621 KB
Loading

0 commit comments

Comments
 (0)