geraete/components/ScanModal.tsx
2025-11-18 14:44:36 +01:00

146 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// components/ScanModal.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import Modal from '@/components/ui/Modal';
import { CameraIcon } from '@heroicons/react/24/outline';
import { BrowserMultiFormatReader, IScannerControls } from '@zxing/browser';
type ScanModalProps = {
open: boolean;
onClose: () => void;
onResult: (code: string) => void;
};
export default function ScanModal({ open, onClose, onResult }: ScanModalProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setError(null);
// Browser / Secure-Context Check
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices ||
typeof navigator.mediaDevices.getUserMedia !== 'function'
) {
setError(
'Kamera wird in diesem Kontext nicht unterstützt. Bitte die Seite über HTTPS oder localhost aufrufen.',
);
return;
}
const codeReader = new BrowserMultiFormatReader();
let controls: IScannerControls | null = null;
let stopped = false;
let rafId: number | null = null;
const startScanner = () => {
// warten, bis das <video> da ist
if (!videoRef.current) {
rafId = requestAnimationFrame(startScanner);
return;
}
codeReader
.decodeFromConstraints(
{
audio: false,
video: {
facingMode: { ideal: 'environment' }, // Rückkamera bevorzugen
},
},
videoRef.current,
(result, err, _controls) => {
if (_controls && !controls) {
controls = _controls;
}
if (result && !stopped) {
stopped = true;
if (controls) controls.stop();
onResult(result.getText());
onClose();
}
if (err && err.name !== 'NotFoundException') {
console.warn('Scanner-Fehler:', err);
}
},
)
.catch((e) => {
console.error('Error starting camera', e);
setError(
'Kamera konnte nicht geöffnet werden. Bitte Berechtigungen prüfen oder einen anderen Browser verwenden.',
);
});
};
startScanner();
return () => {
stopped = true;
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
if (controls) {
controls.stop();
}
// Stream hart stoppen, falls noch aktiv
if (videoRef.current && videoRef.current.srcObject instanceof MediaStream) {
videoRef.current.srcObject.getTracks().forEach((t) => t.stop());
videoRef.current.srcObject = null;
}
// ⚠️ KEIN codeReader.reset() mehr macht bei dir Runtime-Fehler
};
}, [open, onClose, onResult]);
const handleClose = () => onClose();
return (
<Modal
open={open}
onClose={handleClose}
title="QR-Code scannen"
icon={<CameraIcon className="size-6" />}
tone="info"
variant="centered"
size="md"
primaryAction={{
label: 'Abbrechen',
onClick: handleClose,
variant: 'secondary',
}}
>
<div className="mt-4 space-y-3">
{error && (
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
)}
{!error && (
<p className="text-sm text-gray-500 dark:text-gray-400">
Richte deine Kamera auf den QR-Code. Sobald er erkannt wird, öffnet sich das Gerätedetail.
</p>
)}
<div className="mt-2 overflow-hidden rounded-lg border border-gray-200 bg-black dark:border-white/10">
<video
ref={videoRef}
autoPlay
playsInline
muted
className="block w-full h-80 object-contain"
/>
</div>
</div>
</Modal>
);
}