265 lines
10 KiB
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>
|