add QR image upload to send pages
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import Loader from '$lib/components/loader.svelte'
|
||||
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
|
||||
import { decodeQrFromFile } from '$lib/qr.js'
|
||||
|
||||
let {
|
||||
onDecode,
|
||||
disabled = false,
|
||||
}: {
|
||||
onDecode: (text: string) => void
|
||||
disabled?: boolean
|
||||
} = $props()
|
||||
|
||||
let fileInput: HTMLInputElement
|
||||
let busy = $state(false)
|
||||
let error = $state('')
|
||||
|
||||
const handleFile = async (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
busy = true
|
||||
error = ''
|
||||
try {
|
||||
const text = await decodeQrFromFile(file)
|
||||
if (!text) throw new Error('No QR code found in the image')
|
||||
onDecode(text)
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to read QR code'
|
||||
} finally {
|
||||
busy = false
|
||||
if (fileInput) fileInput.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
onchange={handleFile}
|
||||
disabled={disabled || busy} />
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 gap-1.5"
|
||||
disabled={disabled || busy}
|
||||
onclick={() => fileInput?.click()}>
|
||||
{#if busy}
|
||||
<Loader class="size-3.5" />
|
||||
{:else}
|
||||
<QrCodeIcon size={14} />
|
||||
{/if}
|
||||
Scan QR
|
||||
</Button>
|
||||
{#if error}
|
||||
<span class="text-xs text-red-500">{error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
import jsQR from 'jsqr'
|
||||
|
||||
export const decodeQrFromFile = (file: File): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.naturalWidth
|
||||
canvas.height = img.naturalHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Canvas is not supported')
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const { data, width, height } = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
)
|
||||
resolve(jsQR(data, width, height)?.data ?? '')
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
reject(new Error('Could not load the image'))
|
||||
}
|
||||
img.src = url
|
||||
})
|
||||
|
||||
export type QrParseResult = {
|
||||
address: string
|
||||
amount?: string
|
||||
destinationTag?: string
|
||||
}
|
||||
|
||||
// Handles payment URIs (monero:ADDRESS?tx_amount=..., xrp:rADDR?amount=..&dt=..)
|
||||
// as well as bare addresses. Returns the last path-like segment before any
|
||||
// query string so prefixes and /pay paths are skipped.
|
||||
export const parseQrPayload = (
|
||||
text: string,
|
||||
kind: 'xrp' | 'monero',
|
||||
): QrParseResult => {
|
||||
let body = text.trim().replace(/\s+/g, '')
|
||||
body = body.replace(/^(?:[a-z0-9+.-]+):\/?\//i, '')
|
||||
const [beforeQuery, queryRaw] = body.split('?')
|
||||
const segments = beforeQuery.split('/')
|
||||
const address = segments[segments.length - 1]
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
if (queryRaw) {
|
||||
for (const pair of queryRaw.split('&')) {
|
||||
const eq = pair.indexOf('=')
|
||||
const key = (eq >= 0 ? pair.slice(0, eq) : pair).toLowerCase()
|
||||
const value = eq >= 0 ? decodeURIComponent(pair.slice(eq + 1)) : ''
|
||||
if (key) params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (kind === 'xrp') {
|
||||
return {
|
||||
address,
|
||||
amount: params['amount'] || undefined,
|
||||
destinationTag: params['dt'] || undefined,
|
||||
}
|
||||
}
|
||||
return {
|
||||
address,
|
||||
amount: params['tx_amount'] || undefined,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user