118 lines
3.4 KiB
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>
|