mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-28 14:44:15 +00:00
fix: qr scanner to use jsQR and add camera selector
This commit is contained in:
parent
1e0132990a
commit
9f8de81610
6 changed files with 188 additions and 96 deletions
|
|
@ -187,7 +187,7 @@ export default function SubmitForm() {
|
|||
|
||||
<span>or</span>
|
||||
|
||||
<button onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
|
||||
<button type="button" onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2">
|
||||
<Icon icon="mdi:camera" fontSize={20} />
|
||||
Use your camera
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import jsQR from "jsqr";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import QrFinder from "../qr-finder";
|
||||
import QrFinder from "./qr-finder";
|
||||
import { useSelect } from "downshift";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
|
|
@ -15,62 +17,164 @@ interface Props {
|
|||
export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
|
||||
const [permissionGranted, setPermissionGranted] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
||||
|
||||
const webcamRef = useRef<Webcam>(null);
|
||||
const requestRef = useRef<number>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const cameraItems = devices.map((device) => ({
|
||||
value: device.deviceId,
|
||||
label: device.label || `Camera ${device.deviceId.slice(-5)}`,
|
||||
}));
|
||||
|
||||
const {
|
||||
isOpen: isDropdownOpen,
|
||||
getToggleButtonProps,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
selectedItem,
|
||||
} = useSelect({
|
||||
items: cameraItems,
|
||||
selectedItem: cameraItems.find((item) => item.value === selectedDeviceId) ?? null,
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
setSelectedDeviceId(selectedItem?.value ?? null);
|
||||
},
|
||||
});
|
||||
|
||||
const scanQRCode = useCallback(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
// Continue scanning in a loop
|
||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||
|
||||
const webcam = webcamRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!webcam || !canvas) return;
|
||||
|
||||
const video = webcam.video;
|
||||
if (!video || video.videoWidth === 0 || video.videoHeight === 0) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, video.videoWidth, video.videoHeight);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
if (!code) return;
|
||||
|
||||
console.log(code);
|
||||
|
||||
// Cancel animation frame to stop scanning
|
||||
if (requestRef.current) {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
}
|
||||
|
||||
setQrBytesRaw(code.binaryData!);
|
||||
setIsOpen(false);
|
||||
}, [isOpen, setIsOpen, setQrBytesRaw]);
|
||||
|
||||
const requestPermission = async () => {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then(() => setPermissionGranted(true))
|
||||
.catch(() => setPermissionGranted(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
requestPermission();
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const videoDevices = devices.filter((d) => d.kind === "videoinput");
|
||||
setDevices(videoDevices);
|
||||
if (!selectedDeviceId && videoDevices.length > 0) {
|
||||
setSelectedDeviceId(videoDevices[0].deviceId);
|
||||
}
|
||||
});
|
||||
}, [isOpen]);
|
||||
|
||||
const handleScan = (result: IDetectedBarcode[]) => {
|
||||
// todo: fix scan, use jsQR instead, data is wrong
|
||||
setIsOpen(false);
|
||||
|
||||
// Convert to bytes
|
||||
// const encoder = new TextEncoder();
|
||||
// const byteArray = encoder.encode(result[0].rawValue);
|
||||
|
||||
// setQrBytes(byteArray);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isOpen && !permissionGranted) return;
|
||||
requestRef.current = requestAnimationFrame(scanQRCode);
|
||||
}, [permissionGranted]);
|
||||
|
||||
if (isOpen)
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-40 backdrop-brightness-75 backdrop-blur-xs">
|
||||
<div className="bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-xl font-bold">Scan QR Code</h2>
|
||||
<button onClick={() => setIsOpen(false)} className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
||||
<Icon icon="material-symbols:close-rounded" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{devices.length > 0 && (
|
||||
<div className="mb-4 flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold">Camera:</label>
|
||||
<div className="relative w-full">
|
||||
{/* Toggle button to open the dropdown */}
|
||||
<button type="button" {...getToggleButtonProps()} className="pill input w-full !px-2 !py-0.5 !justify-between text-sm">
|
||||
{selectedItem?.label || "Select a camera"}
|
||||
|
||||
<Icon icon="tabler:chevron-down" className="ml-2 size-5" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
<ul
|
||||
{...getMenuProps()}
|
||||
className={`absolute z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg mt-1 shadow-lg max-h-60 overflow-y-auto ${
|
||||
isDropdownOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{isDropdownOpen &&
|
||||
cameraItems.map((item, index) => (
|
||||
<li
|
||||
key={item.value}
|
||||
{...getItemProps({ item, index })}
|
||||
className={`px-4 py-1 cursor-pointer text-sm ${highlightedIndex === index ? "bg-black/15" : ""}`}
|
||||
>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative w-full aspect-square">
|
||||
{permissionGranted === null ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg border-2 border-amber-500">
|
||||
<div className="text-center p-4">
|
||||
<p className="text-red-400 font-bold text-lg mb-2">Camera access denied</p>
|
||||
<p className="text-gray-600">Please allow camera access in your browser settings to scan QR codes</p>
|
||||
</div>
|
||||
{!permissionGranted ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center rounded-2xl border-2 border-amber-500 text-center p-8">
|
||||
<p className="text-red-400 font-bold text-lg mb-2">Camera access denied</p>
|
||||
<p className="text-gray-600">Please allow camera access in your browser settings to scan QR codes</p>
|
||||
<button type="button" onClick={requestPermission} className="pill button text-xs mt-2 !py-0.5 !px-2">
|
||||
Request Permission
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Scanner
|
||||
formats={["qr_code"]}
|
||||
onScan={handleScan}
|
||||
components={{ finder: false }}
|
||||
sound={false}
|
||||
classNames={{ container: "rounded-lg border-2 border-amber-500" }}
|
||||
<Webcam
|
||||
ref={webcamRef}
|
||||
audio={false}
|
||||
videoConstraints={{
|
||||
deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined,
|
||||
}}
|
||||
className="size-full object-cover rounded-2xl border-2 border-amber-500"
|
||||
/>
|
||||
|
||||
<QrFinder />
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button onClick={() => setIsOpen(false)} className="pill button">
|
||||
<button type="button" onClick={() => setIsOpen(false)} className="pill button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { FileWithPath, useDropzone } from "react-dropzone";
|
||||
import { Icon } from "@iconify/react";
|
||||
import jsQR from "jsqr";
|
||||
|
|
@ -10,6 +10,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
// Scan QR code
|
||||
|
|
@ -17,7 +19,9 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
reader.onload = async (event) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
|
|
@ -26,9 +30,10 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
||||
const decoded = jsQR(imageData.data, image.width, image.height);
|
||||
const code = jsQR(imageData.data, image.width, image.height);
|
||||
if (!code) return;
|
||||
|
||||
setQrBytesRaw(decoded?.binaryData!);
|
||||
setQrBytesRaw(code.binaryData!);
|
||||
};
|
||||
image.src = event.target!.result as string;
|
||||
};
|
||||
|
|
@ -59,6 +64,8 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
|
|||
or click to open
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue