111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
import { useState } from 'react'
|
||
import { Button, Dialog } from '@/components/ui'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import { useBarcodeReader, type BarcodeReaderStatus } from './useBarcodeReader'
|
||
|
||
export type BarcodeScannerDialogProps = {
|
||
open: boolean
|
||
onDialogClose: () => void
|
||
/**
|
||
* Okunan kod. `false` dönerse kod reddedilmiş sayılır ve listeye yazılmaz;
|
||
* sürekli modda yazma hatasını kullanıcıya göstermek için kullanılır.
|
||
*/
|
||
onDetect: (code: string) => boolean | void | Promise<boolean | void>
|
||
/** Açıkken diyalog okumadan sonra kapanmaz, okunanları listeler. */
|
||
continuous?: boolean
|
||
title?: string
|
||
}
|
||
|
||
const statusMessageKeys: Record<BarcodeReaderStatus, string> = {
|
||
starting: '::App.BarcodeScanner.Starting',
|
||
scanning: '::App.BarcodeScanner.Aim',
|
||
denied: '::App.BarcodeScanner.PermissionDenied',
|
||
insecure: '::App.BarcodeScanner.InsecureContext',
|
||
standaloneIos: '::App.BarcodeScanner.StandaloneIos',
|
||
error: '::App.BarcodeScanner.CameraError',
|
||
}
|
||
|
||
/**
|
||
* Kamerayla barkod/QR okuma diyaloğu. Tek okuma ve sürekli tarama aynı
|
||
* bileşendir; fark yalnızca okumadan sonra kapanıp kapanmadığıdır.
|
||
*/
|
||
const BarcodeScannerDialog = ({
|
||
open,
|
||
onDialogClose,
|
||
onDetect,
|
||
continuous = false,
|
||
title,
|
||
}: BarcodeScannerDialogProps) => {
|
||
const { translate } = useLocalization()
|
||
const [scanned, setScanned] = useState<string[]>([])
|
||
const [failed, setFailed] = useState<string>()
|
||
|
||
const handleDetect = async (code: string) => {
|
||
const accepted = await onDetect(code)
|
||
if (accepted === false) {
|
||
setFailed(code)
|
||
return
|
||
}
|
||
|
||
if (!continuous) {
|
||
onDialogClose()
|
||
return
|
||
}
|
||
|
||
setFailed(undefined)
|
||
setScanned((previous) => [code, ...previous])
|
||
}
|
||
|
||
const { videoRef, status } = useBarcodeReader({ active: open, onDetect: handleDetect })
|
||
|
||
return (
|
||
<Dialog isOpen={open} width={520} onClose={onDialogClose} onRequestClose={onDialogClose}>
|
||
<h5 className="mb-4">{title ?? translate('::App.BarcodeScanner.Title')}</h5>
|
||
|
||
<div className="relative overflow-hidden rounded-lg bg-black">
|
||
<video
|
||
ref={videoRef}
|
||
autoPlay
|
||
muted
|
||
playsInline
|
||
className="h-64 w-full object-cover sm:h-80"
|
||
/>
|
||
{status === 'scanning' && (
|
||
// Nişangah: kullanıcıya kodu nereye tutacağını gösterir.
|
||
<div className="pointer-events-none absolute inset-x-10 inset-y-16 rounded-lg border-2 border-white/80" />
|
||
)}
|
||
</div>
|
||
|
||
<p className="mt-3 text-sm opacity-80">{translate(statusMessageKeys[status])}</p>
|
||
|
||
{failed && (
|
||
<p className="mt-1 text-sm text-red-500">
|
||
{translate('::App.BarcodeScanner.SaveFailed')} ({failed})
|
||
</p>
|
||
)}
|
||
|
||
{continuous && scanned.length > 0 && (
|
||
<div className="mt-3">
|
||
<p className="mb-1 text-sm font-semibold">
|
||
{translate('::App.BarcodeScanner.ScannedCount')}: {scanned.length}
|
||
</p>
|
||
<ul className="max-h-32 overflow-auto rounded border text-sm">
|
||
{scanned.map((code, index) => (
|
||
<li key={`${code}-${index}`} className="border-b px-2 py-1 last:border-b-0">
|
||
{code}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-5 text-right">
|
||
<Button variant="solid" onClick={onDialogClose}>
|
||
{translate(continuous ? '::App.Platform.Close' : '::App.Platform.Cancel')}
|
||
</Button>
|
||
</div>
|
||
</Dialog>
|
||
)
|
||
}
|
||
|
||
export default BarcodeScannerDialog
|