Skip to content

Commit

Permalink
Replace with qr-scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
val-samonte committed Feb 2, 2025
1 parent e9c499c commit b25e002
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 109 deletions.
62 changes: 16 additions & 46 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"@types/node": "18.16.1",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.1",
"@zxing/browser": "^0.1.5",
"autoprefixer": "10.4.14",
"bs58": "^5.0.0",
"classnames": "^2.3.2",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"next": "^15.1.6",
"postcss": "^8.5.1",
"qr-scanner": "^1.4.2",
"qrcode.react": "^4.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
38 changes: 9 additions & 29 deletions src/app/QRDragAndDrop.tsx → src/app/QRDragDrop.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,19 @@
import React, { useState, useRef, DragEvent, ReactNode } from 'react'
import { BrowserQRCodeReader } from '@zxing/browser'
import QrScanner from 'qr-scanner'

interface QRDragAndDropProps {
interface QRDragDropProps {
children: ReactNode
onQRCodesDetected: (qrCodes: (string | null)[]) => void
className?: string
}

const loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.src = src
})
}

const createCanvas = (img: HTMLImageElement): HTMLCanvasElement => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
context?.drawImage(img, 0, 0)
return canvas
}

export const QRDragAndDrop: React.FC<QRDragDropProps> = ({
const QRDragDrop: React.FC<QRDragDropProps> = ({
children,
onQRCodesDetected,
className = '',
}) => {
const [isDragging, setIsDragging] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const reader = new BrowserQRCodeReader()

const processFiles = async (files: FileList) => {
const qrCodes: (string | null)[] = []
Expand All @@ -42,13 +23,10 @@ export const QRDragAndDrop: React.FC<QRDragDropProps> = ({
if (!file.type.includes('png')) continue

try {
const imageUrl = URL.createObjectURL(file)
const img = await loadImage(imageUrl)
const canvas = createCanvas(img)
const result = await reader.decodeFromCanvas(canvas)

qrCodes.push(result.getText())
URL.revokeObjectURL(imageUrl)
const result = await QrScanner.scanImage(file, {
returnDetailedScanResult: true,
})
qrCodes.push(result.data)
} catch (error) {
console.error('Error processing file:', error)
qrCodes.push(null)
Expand Down Expand Up @@ -118,3 +96,5 @@ export const QRDragAndDrop: React.FC<QRDragDropProps> = ({
</div>
)
}

export default QRDragDrop
68 changes: 41 additions & 27 deletions src/app/QRScanner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react'
import { BrowserQRCodeReader } from '@zxing/browser'
import QrScanner from 'qr-scanner'

interface QRScannerProps {
onQRCodeScanned: (text: string) => void
Expand All @@ -11,50 +11,64 @@ export const QRScanner: React.FC<QRScannerProps> = ({
onError,
}) => {
const videoRef = useRef<HTMLVideoElement>(null)
const codeReader = useRef(new BrowserQRCodeReader())
const scannerRef = useRef<QrScanner | null>(null)

useEffect(() => {
const startScanning = async () => {
try {
const videoInputDevices =
await BrowserQRCodeReader.listVideoInputDevices()
const cameras = await QrScanner.listCameras()

if (videoInputDevices.length === 0) {
if (cameras.length === 0) {
onError('No camera devices found. Tap to retry.')
return
}

// Select the back camera if available, otherwise default to the first device
const selectedDevice =
videoInputDevices.find(
(device) =>
device.label.toLowerCase().includes('back') ||
device.label.toLowerCase().includes('rear'),
) || videoInputDevices[0]
// Select the back camera if available, otherwise default to the first camera
const selectedCamera =
cameras.find(
(camera) =>
camera.label.toLowerCase().includes('back') ||
camera.label.toLowerCase().includes('rear'),
) || cameras[0]

const selectedDeviceId = selectedDevice.deviceId
if (!videoRef.current) return

if (videoRef.current) {
await codeReader.current.decodeFromVideoDevice(
selectedDeviceId,
videoRef.current,
(result, error) => {
if (result) {
onQRCodeScanned(result.getText())
}
if (error) {
// Ignore errors as they occur frequently when no QR code is detected
return
}
},
)
// Stop any existing scanner
if (scannerRef.current) {
scannerRef.current.destroy()
}

// Create new scanner
scannerRef.current = new QrScanner(
videoRef.current,
(result) => {
onQRCodeScanned(result.data)
},
{
preferredCamera: selectedCamera.id,
highlightScanRegion: false,
highlightCodeOutline: false,
returnDetailedScanResult: true,
},
)

await scannerRef.current.start()
} catch (error) {
console.error('Error accessing camera:', error)
onError(
'Failed to access camera. Please ensure camera permissions are granted.',
)
}
}

startScanning()

// Cleanup function
return () => {
if (scannerRef.current) {
scannerRef.current.destroy()
}
}
}, [onQRCodeScanned, onError])

return (
Expand Down
15 changes: 9 additions & 6 deletions src/app/decrypt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { combine } from 'shamirs-secret-sharing-ts'
import { decrypt } from '../crypto'
import CloseIcon from '../CloseIcon'
import { QRScanner } from '../QRScanner'
import { QRDragAndDrop } from '../QRDragAndDrop'
import QRDragDrop from '../QRDragDrop'

export default function DecryptPage() {
const [scans, setScans] = useState<string[]>([])
Expand Down Expand Up @@ -51,15 +51,17 @@ export default function DecryptPage() {

try {
// QR Codes
shares = scans.map((s) => Buffer.from(bs58.decode(s)))
shares = scans.map((s) =>
Buffer.from(bs58.decode(s)),
) as unknown as Uint8Array[]
} catch (e: any) {
setHasError('QR Code: ' + e.message)
return
}

try {
// Shamir's Secret Sharing
recovered = combine(shares!)
recovered = combine(shares!) as unknown as Uint8Array
} catch (e: any) {
setHasError('SSS: ' + e.message)
return
Expand All @@ -78,10 +80,11 @@ export default function DecryptPage() {
// AES-GCM
data = await decrypt(salt, password, ciphertext)
} catch (e: any) {
console.error(e)
if (e.message.includes('base58')) {
setHasError('Incomplete QR Codes')
} else {
setHasError('AES-GCM: ' + e.message)
setHasError('AES-GCM: ' + (e.message || 'Possible incorrect password'))
}
return
}
Expand All @@ -104,7 +107,7 @@ export default function DecryptPage() {
<span>{scans.length} codes parsed</span>
</label>
<div className='relative'>
<QRDragAndDrop
<QRDragDrop
onQRCodesDetected={(uploads) => {
uploads.forEach((file) => {
if (file) {
Expand All @@ -128,7 +131,7 @@ export default function DecryptPage() {
setErrorMsg('')
}}
/>
</QRDragAndDrop>
</QRDragDrop>
{scans.length === 0 && (
<div className='absolute inset-x-0 bottom-0 flex items-center justify-center p-3'>
<div className='px-3 py-2 mt-2 text-xs uppercase rounded-md'>
Expand Down

0 comments on commit b25e002

Please sign in to comment.