165 lines
6.5 KiB
TypeScript
165 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Camera, History, VideoOff } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; // Assuming shadcn/ui has Select
|
|
import { SHOE_DATABASE, type Shoe } from '@/lib/shoe-database';
|
|
import { detectShoe } from '@/lib/ml-classification';
|
|
import { addToHistory, getHistory } from '@/lib/history-storage';
|
|
import ShoeResultsPopup from '@/components/shoe-results-popup';
|
|
import HistorySidebar from '@/components/history-sidebar';
|
|
|
|
type CameraStatus = 'idle' | 'active' | 'denied' | 'no_devices';
|
|
|
|
export default function HomePage() {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
const [cameraStatus, setCameraStatus] = useState<CameraStatus>('idle');
|
|
|
|
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
|
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
|
|
|
|
const [activeShoe, setActiveShoe] = useState<Shoe | null>(null);
|
|
const [isPopupOpen, setPopupOpen] = useState(false);
|
|
const [isHistoryOpen, setHistoryOpen] = useState(false);
|
|
const [history, setHistory] = useState<Shoe[]>([]);
|
|
|
|
// Effect to clean up the stream when component unmounts or stream changes
|
|
useEffect(() => {
|
|
return () => {
|
|
stream?.getTracks().forEach((track) => track.stop());
|
|
};
|
|
}, [stream]);
|
|
|
|
const startStream = async (deviceId: string) => {
|
|
// Stop previous stream if it exists
|
|
stream?.getTracks().forEach((track) => track.stop());
|
|
|
|
try {
|
|
const newStream = await navigator.mediaDevices.getUserMedia({
|
|
video: { deviceId: { exact: deviceId } },
|
|
});
|
|
if (videoRef.current) {
|
|
videoRef.current.srcObject = newStream;
|
|
}
|
|
setStream(newStream);
|
|
setCameraStatus('active');
|
|
} catch (err) {
|
|
console.error("Error starting stream: ", err);
|
|
setCameraStatus('denied');
|
|
}
|
|
};
|
|
|
|
const handleOpenCamera = async () => {
|
|
setHistory(getHistory());
|
|
try {
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const videoInputs = devices.filter((d) => d.kind === 'videoinput');
|
|
if (videoInputs.length === 0) {
|
|
setCameraStatus('no_devices');
|
|
return;
|
|
}
|
|
setVideoDevices(videoInputs);
|
|
const firstDeviceId = videoInputs[0].deviceId;
|
|
setSelectedDeviceId(firstDeviceId);
|
|
await startStream(firstDeviceId);
|
|
} catch (err) {
|
|
console.error("Error enumerating devices: ", err);
|
|
setCameraStatus('denied');
|
|
}
|
|
};
|
|
|
|
const handleCameraChange = (deviceId: string) => {
|
|
setSelectedDeviceId(deviceId);
|
|
startStream(deviceId);
|
|
};
|
|
|
|
const handleScan = () => {
|
|
const detected = detectShoe(SHOE_DATABASE);
|
|
if (detected) {
|
|
setActiveShoe(detected);
|
|
const updatedHistory = addToHistory(detected);
|
|
setHistory(updatedHistory);
|
|
setPopupOpen(true);
|
|
}
|
|
};
|
|
|
|
const handleHistoryItemClick = (shoe: Shoe) => {
|
|
setActiveShoe(shoe);
|
|
setHistoryOpen(false);
|
|
setPopupOpen(true);
|
|
};
|
|
|
|
const renderContent = () => {
|
|
switch (cameraStatus) {
|
|
case 'active':
|
|
return (
|
|
<>
|
|
<video ref={videoRef} autoPlay playsInline muted onCanPlay={() => videoRef.current?.play()} className="h-full w-full object-cover" />
|
|
<div className="absolute inset-0 flex flex-col items-center justify-between p-6">
|
|
<div className="flex w-full items-start justify-between gap-2">
|
|
<div className="w-64">
|
|
<Select value={selectedDeviceId} onValueChange={handleCameraChange}>
|
|
<SelectTrigger className="w-full bg-black/50 text-white border-white/30">
|
|
<SelectValue placeholder="Seleccionar cámara..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{videoDevices.map((device) => (
|
|
<SelectItem key={device.deviceId} value={device.deviceId}>
|
|
{device.label || `Cámara ${videoDevices.indexOf(device) + 1}`}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button variant="ghost" size="icon" className="text-white hover:bg-white/20 hover:text-white" onClick={() => setHistoryOpen(true)}>
|
|
<History size={24} />
|
|
</Button>
|
|
</div>
|
|
<div className="flex flex-col items-center">
|
|
<Button size="lg" className="h-16 w-16 rounded-full border-4 border-white bg-transparent text-white shadow-lg hover:bg-white/20" onClick={handleScan}>
|
|
<Camera size={32} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
case 'no_devices':
|
|
return (
|
|
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
|
<VideoOff size={48} className="mb-4 text-red-500" />
|
|
<h1 className="text-xl font-semibold">No se encontraron cámaras</h1>
|
|
<p className="text-gray-400">Asegúrate de que tu cámara esté conectada.</p>
|
|
</div>
|
|
);
|
|
case 'denied':
|
|
return (
|
|
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
|
<Camera size={48} className="mb-4 text-red-500" />
|
|
<h1 className="text-xl font-semibold">Acceso a la cámara denegado</h1>
|
|
<p className="text-gray-400">Por favor, habilita el permiso en tu navegador.</p>
|
|
</div>
|
|
);
|
|
case 'idle':
|
|
default:
|
|
return (
|
|
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-900 text-white">
|
|
<h1 className="mb-4 text-3xl font-bold">Detector de Zapatos</h1>
|
|
<p className="mb-8 text-gray-400">Haz clic para iniciar la cámara y escanear</p>
|
|
<Button size="lg" onClick={handleOpenCamera}>
|
|
<Camera className="mr-2 h-4 w-4" /> Abrir Cámara
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<main className="relative h-screen w-screen bg-black overflow-hidden">
|
|
{renderContent()}
|
|
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} shoe={activeShoe} />
|
|
<HistorySidebar isOpen={isHistoryOpen} onOpenChange={setHistoryOpen} history={history} onItemClick={handleHistoryItemClick} />
|
|
</main>
|
|
);
|
|
} |