webapp/src/components/ui/qr-scanner.vue

118 lines
3.4 KiB
Vue

<template>
<div class="relative">
<!-- Camera Permission Request -->
<div v-if="hasPermission === false" class="text-center p-6 space-y-4">
<div class="text-destructive">
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</div>
<div>
<h3 class="font-medium text-lg">Camera Access Required</h3>
<p class="text-sm text-muted-foreground mt-1">{{ error || 'Please allow camera access to scan QR codes' }}</p>
</div>
<Button @click="requestPermission" variant="outline">
Try Again
</Button>
</div>
<!-- Scanner Interface -->
<div v-else class="space-y-4">
<!-- Video Element -->
<div class="relative bg-black rounded-lg overflow-hidden">
<video
ref="videoEl"
class="w-full h-64 sm:h-80 object-cover"
muted
playsinline
></video>
<!-- Loading state -->
<div v-if="!isScanning && hasPermission === true" class="absolute inset-0 flex items-center justify-center bg-black/50">
<div class="text-center text-white">
<Loader2 class="w-8 h-8 animate-spin mx-auto mb-2" />
<p class="text-sm">Starting camera...</p>
</div>
</div>
</div>
<!-- Controls -->
<div class="flex items-center justify-between">
<div class="text-sm text-muted-foreground">
{{ isScanning ? 'Point camera at QR code' : 'Preparing scanner...' }}
</div>
<div class="flex gap-2">
<!-- Flash toggle (if available) -->
<Button
v-if="flashAvailable"
@click="toggleFlash"
variant="outline"
size="sm"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</Button>
<!-- Close scanner -->
<Button @click="$emit('close')" variant="outline" size="sm">
Close
</Button>
</div>
</div>
<!-- Error display -->
<div v-if="error" class="text-destructive text-sm p-3 bg-destructive/10 rounded-md">
{{ error }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { Button } from '@/components/ui/button'
import { Loader2 } from 'lucide-vue-next'
import { useQRScanner } from '@/composables/useQRScanner'
interface Emits {
(e: 'result', value: string): void
(e: 'close'): void
}
const emit = defineEmits<Emits>()
const videoEl = ref<HTMLVideoElement>()
const flashAvailable = ref(false)
const {
isScanning,
hasPermission,
error,
startScanning,
stopScanning,
toggleFlash,
hasFlash
} = useQRScanner()
const requestPermission = async () => {
if (videoEl.value) {
await startScanning(videoEl.value, (result) => {
emit('result', result)
})
flashAvailable.value = await hasFlash()
}
}
onMounted(async () => {
await nextTick()
if (videoEl.value) {
await requestPermission()
}
})
onUnmounted(() => {
stopScanning()
})
</script>