feat(mobile): add local QR scanning and desktop pairing invitations

This commit is contained in:
yilsem committed 2026-09-08 21:28:24 +03:00
1 parent c1929957a2
commit 4115f1b4c0
17 files changed
+178 -17

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import qrcode from './vendor/qrcode.mjs'
export function drawQr(canvas, text) {
const qr = qrcode(0, 'M'); qr.addData(text); qr.make()
const count = qr.getModuleCount(), scale = 5, margin = 4
canvas.width = canvas.height = (count + margin * 2) * scale
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#000'
for (let y = 0; y < count; y++) for (let x = 0; x < count; x++)
if (qr.isDark(y, x)) ctx.fillRect((x + margin) * scale, (y + margin) * scale, scale, scale)
}
export class InviteScanner {
constructor(video, onCode) { this.video = video; this.onCode = onCode; this.epoch = 0; this.canvas = document.createElement('canvas') }
decode(source, width, height) {
if (!width || !height) return null
const scale = Math.min(1, 1280 / Math.max(width, height))
this.canvas.width = Math.round(width * scale); this.canvas.height = Math.round(height * scale)
const ctx = this.canvas.getContext('2d', { willReadFrequently: true })
ctx.drawImage(source, 0, 0, this.canvas.width, this.canvas.height)
const pixels = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
return globalThis.jsQR(pixels.data, pixels.width, pixels.height)?.data
}
async image(file) {
this.stop(); const epoch = this.epoch
if (!file || file.size > 8 * 1024 * 1024) throw new Error('QR resmi en fazla 8 MB olabilir.')
const bitmap = await createImageBitmap(file)
let result
try { result = this.decode(bitmap, bitmap.width, bitmap.height) } finally { bitmap.close() }
if (epoch !== this.epoch) return
if (!result) throw new Error('Resimde QR okunamadı; daha net bir görüntü deneyin.')
await this.onCode(result)
}
async start() {
this.stop(); const epoch = this.epoch
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
if (epoch !== this.epoch) { stream.getTracks().forEach(t => t.stop()); return }
this.stream = stream; this.video.srcObject = stream; this.video.hidden = false
try {
await this.video.play()
if (epoch !== this.epoch) return
this.timer = setInterval(() => {
const result = this.decode(this.video, this.video.videoWidth, this.video.videoHeight)
if (result) { this.stop(); void this.onCode(result).catch(error => this.onError?.(error)) }
}, 250)
this.limit = setTimeout(() => this.stop(), 60000)
} catch (error) { this.stop(); throw error }
}
stop() { this.epoch++; clearInterval(this.timer); clearTimeout(this.limit); this.stream?.getTracks().forEach(t => t.stop()); this.stream = null; this.video.srcObject = null; this.video.hidden = true }
}