Files
Final-Year-Project/WebApp/src/routes/camera/+page.svelte

265 lines
10 KiB
Svelte

<script lang="ts">
// @ts-nocheck
import AppChrome from '$lib/sim/ui/AppChrome.svelte';
import { appController } from '$lib/app/controller';
import { appState } from '$lib/app/store';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import {
VideoOff,
RefreshCw,
Zap,
Square,
Activity,
Settings2,
Terminal,
Camera
} from '@lucide/svelte';
let cameraVideoElement: HTMLVideoElement | null = null;
$effect(() => {
appController.setCameraVideoElement(cameraVideoElement);
});
const cameraInputItems = () =>
$appState.cameraInputDevices.map((cameraInput) => ({
value: cameraInput.id,
label: cameraInput.label
}));
const selectedCameraInputLabel = () =>
$appState.cameraInputDevices.find((cameraInput) => cameraInput.id === $appState.selectedCameraInputId)?.label ||
($appState.cameraInputDevices.length > 0 ? 'Choose camera input' : 'No camera inputs found');
const motionDetectionProfileItems = [
{ value: 'low_power', label: 'Low Power' },
{ value: 'balanced', label: 'Balanced' },
{ value: 'responsive', label: 'Responsive' }
];
const motionDetectionProfileLabel = () =>
motionDetectionProfileItems.find((item) => item.value === $appState.motionDetection.profile)?.label || 'Balanced';
</script>
<AppChrome pageKey="camera">
<section id="screen-home-camera" class="mx-auto flex h-full min-h-0 max-w-7xl flex-col gap-4 lg:gap-6">
<div class="flex flex-col gap-4 lg:gap-6 xl:flex-1 xl:min-h-0">
<Card variant="glass" class="relative flex min-h-[220px] overflow-hidden sm:min-h-[260px] xl:flex-[3]">
<div id="cameraPreview" class="flex-1 bg-black relative flex items-center justify-center {$appState.isMotionActive ? 'bg-red-900/20' : ''}">
<video
id="cameraVideo"
class="absolute inset-0 w-full h-full object-cover {$appState.cameraPreviewReady ? '' : 'hidden'}"
autoplay
playsinline
muted
bind:this={cameraVideoElement}
></video>
<Badge
variant="destructive"
class="absolute top-4 left-4 z-20 gap-2 rounded-full border border-white/10 bg-black/50 px-3 py-1.5 backdrop-blur"
>
<span class="w-2 h-2 bg-red-500 rounded-full shadow-[0_0_8px_rgba(239,68,68,0.8)] animate-pulse"></span>
<span class="text-[10px] font-bold tracking-widest">REC</span>
</Badge>
</div>
<div
id="cameraOfflineOverlay"
class="absolute inset-0 bg-[#0a0a0c]/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center gap-4 {$appState.socketConnected ? 'hidden' : ''}"
>
<Alert variant="default" class="w-full max-w-sm border-white/10 bg-[#141418]/90 text-center shadow-xl">
<VideoOff class="mx-auto h-12 w-12 text-gray-600 mb-2" />
<AlertTitle class="text-base text-white">Camera Offline</AlertTitle>
<AlertDescription class="text-sm text-gray-400">
Reconnect this device to resume live monitoring.
</AlertDescription>
<div class="mt-4 flex justify-center">
<Button
id="cameraGoOnlineBtn"
variant="outline"
class="rounded-xl border-emerald-500/50 text-emerald-400 hover:border-emerald-400 hover:bg-emerald-500/10"
onclick={() => appController.goOnline()}
>
Go Online
</Button>
</div>
</Alert>
</div>
</Card>
<div class="grid grid-cols-1 gap-4 lg:gap-6 xl:min-h-0 xl:flex-[2] xl:grid-cols-[2fr_1fr]">
<Card variant="glass" class="min-h-[220px] overflow-hidden xl:min-h-0">
<CardHeader class="pb-2 flex flex-row items-center gap-2">
<Terminal class="size-3.5 text-gray-500" />
<CardTitle class="text-[10px] font-bold uppercase tracking-widest text-gray-500">Activity Logs</CardTitle>
</CardHeader>
<CardContent class="flex-1 xl:min-h-0">
<ScrollArea
id="cameraLogs"
class="h-[220px] rounded-xl border border-white/5 bg-black/40 p-4 text-xs font-mono text-gray-400 xl:h-full xl:min-h-[220px]"
>
{#if $appState.activityLog.length === 0}
<div class="text-gray-600 italic">Awaiting connection...</div>
{:else}
{#each $appState.activityLog as log (log.id)}
<div class="mb-1 leading-relaxed">
<span class="text-gray-600">[{new Date(log.createdAt).toLocaleTimeString()}]</span>
<span class="text-blue-400/80">{log.type}:</span>
<span>{log.message}</span>
</div>
{/each}
{/if}
</ScrollArea>
</CardContent>
</Card>
<Card variant="glass" class="min-h-[220px] overflow-hidden xl:min-h-0">
<CardHeader class="pb-2 flex flex-row items-center gap-2">
<Settings2 class="size-3.5 text-gray-500" />
<CardTitle class="text-[10px] font-bold uppercase tracking-widest text-gray-500">Control Plane</CardTitle>
</CardHeader>
<CardContent class="space-y-4 xl:min-h-0 xl:overflow-y-auto">
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<Label for="cameraInputSelect" class="text-[10px] font-bold uppercase tracking-widest text-gray-400 flex items-center gap-1.5">
<Camera class="size-3" />
Camera Input
</Label>
<Button
id="refreshCameraInputsBtn"
variant="ghost"
size="xs"
class="h-6 rounded-lg text-gray-500 hover:text-white"
onclick={() => appController.refreshCameraInputs()}
>
<RefreshCw class="size-3 mr-1" />
Refresh
</Button>
</div>
<Select
type="single"
value={$appState.selectedCameraInputId}
items={cameraInputItems()}
onValueChange={(value) => appController.selectCameraInput(value)}
>
<SelectTrigger
id="cameraInputSelect"
class="h-10 w-full rounded-xl border-white/10 bg-black/40 text-gray-200"
>
{selectedCameraInputLabel()}
</SelectTrigger>
<SelectContent>
{#each $appState.cameraInputDevices as cameraInput (cameraInput.id)}
<SelectItem value={cameraInput.id} label={cameraInput.label} />
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-3 rounded-2xl border border-white/10 bg-black/30 p-3 sm:p-4 sm:space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="space-y-1">
<p class="text-[10px] font-bold uppercase tracking-widest text-gray-400">Automatic Detection</p>
<p class="text-sm text-gray-200 font-medium">
{$appState.motionDetection.enabled ? 'Armed' : 'Disarmed'}
</p>
<p class="text-[10px] text-gray-500 leading-tight">
Foreground browser monitoring.
</p>
</div>
<Button
variant={$appState.motionDetection.enabled ? 'secondary' : 'outline'}
class="min-w-[80px] h-9 self-start rounded-xl sm:self-auto text-xs"
onclick={() => appController.setMotionDetectionEnabled(!$appState.motionDetection.enabled)}
>
{$appState.motionDetection.enabled ? 'Pause' : 'Arm'}
</Button>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<Label for="motionDetectionProfile" class="text-[10px] font-bold uppercase tracking-widest text-gray-400">
Profile
</Label>
<span class="text-[10px] font-bold uppercase tracking-widest text-blue-500/80">{$appState.motionDetection.state}</span>
</div>
<Select
type="single"
value={$appState.motionDetection.profile}
items={motionDetectionProfileItems}
onValueChange={(value) => appController.setMotionDetectionProfile(value)}
>
<SelectTrigger
id="motionDetectionProfile"
class="h-10 w-full rounded-xl border-white/10 bg-black/40 text-gray-200"
>
{motionDetectionProfileLabel()}
</SelectTrigger>
<SelectContent>
{#each motionDetectionProfileItems as item (item.value)}
<SelectItem value={item.value} label={item.label} />
{/each}
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-xl border border-white/5 bg-white/5 px-3 py-2">
<p class="text-[9px] font-bold uppercase tracking-widest text-gray-500">Detector</p>
<p class="mt-0.5 text-xs font-bold text-gray-200">{$appState.motionDetection.state}</p>
</div>
<div class="rounded-xl border border-white/5 bg-white/5 px-3 py-2">
<p class="text-[9px] font-bold uppercase tracking-widest text-gray-500">Score</p>
<p class="mt-0.5 text-xs font-bold text-gray-200">{$appState.motionDetection.score.toFixed(3)}</p>
</div>
</div>
<div class="flex items-center justify-between gap-3 rounded-xl border border-white/5 bg-white/5 px-3 py-2">
<div class="space-y-0.5">
<p class="text-xs font-bold text-gray-200">Debug Overlay</p>
<p class="text-[10px] text-gray-500">Sample profiles & metrics.</p>
</div>
<Switch
checked={$appState.motionDetection.debug}
onCheckedChange={(v) => appController.setMotionDetectionDebug(v)}
/>
</div>
</div>
<div class="space-y-2">
{#if !$appState.isMotionActive}
<Button
id="startMotionBtn"
variant="outline"
class="h-12 w-full justify-start rounded-xl border-white/5 bg-white/5 font-bold text-sm hover:border-blue-500/30 hover:bg-blue-500/10 hover:text-blue-400 group"
onclick={() => appController.startMotion()}
>
<Zap class="mr-3 h-4 w-4 text-gray-400 group-hover:text-blue-400" />
Simulate Motion
</Button>
{:else}
<Button
id="endMotionBtn"
variant="outline"
class="h-12 w-full justify-start rounded-xl border-white/5 bg-white/5 font-bold text-sm hover:bg-white/10"
onclick={() => appController.endMotion()}
>
<Square class="mr-3 h-4 w-4 text-red-500 animate-pulse" />
Stop Recording
</Button>
{/if}
</div>
</CardContent>
</Card>
</div>
</div>
</section>
</AppChrome>