mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 12:15:25 +03:00
Simplify basic image view
This commit is contained in:
parent
5bb382703a
commit
a004890a7e
@ -18,7 +18,6 @@ export default function AutoUpdatingCameraImage({
|
|||||||
showFps = true,
|
showFps = true,
|
||||||
className,
|
className,
|
||||||
reloadInterval = MIN_LOAD_TIMEOUT_MS,
|
reloadInterval = MIN_LOAD_TIMEOUT_MS,
|
||||||
fitAspect,
|
|
||||||
}: AutoUpdatingCameraImageProps) {
|
}: AutoUpdatingCameraImageProps) {
|
||||||
const [key, setKey] = useState(Date.now());
|
const [key, setKey] = useState(Date.now());
|
||||||
const [fps, setFps] = useState<string>("0");
|
const [fps, setFps] = useState<string>("0");
|
||||||
@ -43,7 +42,6 @@ export default function AutoUpdatingCameraImage({
|
|||||||
<CameraImage
|
<CameraImage
|
||||||
camera={camera}
|
camera={camera}
|
||||||
onload={handleLoad}
|
onload={handleLoad}
|
||||||
fitAspect={fitAspect}
|
|
||||||
searchParams={`cache=${key}&${searchParams}`}
|
searchParams={`cache=${key}&${searchParams}`}
|
||||||
/>
|
/>
|
||||||
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "../ui/activity-indicator";
|
import ActivityIndicator from "../ui/activity-indicator";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
|
||||||
|
|
||||||
type CameraImageProps = {
|
type CameraImageProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
camera: string;
|
camera: string;
|
||||||
onload?: (event: Event) => void;
|
onload?: () => void;
|
||||||
searchParams?: {};
|
searchParams?: {};
|
||||||
stretch?: boolean; // stretch to fit width
|
|
||||||
fitAspect?: number; // shrink to fit height
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CameraImage({
|
export default function CameraImage({
|
||||||
@ -18,73 +15,25 @@ export default function CameraImage({
|
|||||||
camera,
|
camera,
|
||||||
onload,
|
onload,
|
||||||
searchParams = "",
|
searchParams = "",
|
||||||
stretch = false,
|
|
||||||
fitAspect,
|
|
||||||
}: CameraImageProps) {
|
}: CameraImageProps) {
|
||||||
const { data: config } = useSWR("config");
|
const { data: config } = useSWR("config");
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const [hasLoaded, setHasLoaded] = useState(false);
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
const [{ width: containerWidth, height: containerHeight }] =
|
|
||||||
useResizeObserver(containerRef);
|
|
||||||
|
|
||||||
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
|
|
||||||
// https://github.com/blakeblackshear/frigate/issues/1657
|
|
||||||
let scrollBarWidth = 0;
|
|
||||||
if (window.innerWidth && document.body.offsetWidth) {
|
|
||||||
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
|
|
||||||
}
|
|
||||||
const availableWidth = scrollBarWidth
|
|
||||||
? containerWidth + scrollBarWidth
|
|
||||||
: containerWidth;
|
|
||||||
|
|
||||||
const { name } = config ? config.cameras[camera] : "";
|
const { name } = config ? config.cameras[camera] : "";
|
||||||
const enabled = config ? config.cameras[camera].enabled : "True";
|
const enabled = config ? config.cameras[camera].enabled : "True";
|
||||||
const { width, height } = config
|
|
||||||
? config.cameras[camera].detect
|
|
||||||
: { width: 1, height: 1 };
|
|
||||||
const aspectRatio = width / height;
|
|
||||||
|
|
||||||
const scaledHeight = useMemo(() => {
|
|
||||||
const scaledHeight =
|
|
||||||
aspectRatio < (fitAspect ?? 0)
|
|
||||||
? Math.floor(containerHeight)
|
|
||||||
: Math.floor(availableWidth / aspectRatio);
|
|
||||||
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
|
|
||||||
|
|
||||||
if (finalHeight > 0) {
|
|
||||||
return finalHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 100;
|
|
||||||
}, [availableWidth, aspectRatio, height, stretch]);
|
|
||||||
const scaledWidth = useMemo(
|
|
||||||
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
|
|
||||||
[scaledHeight, aspectRatio, scrollBarWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
const img = useMemo(() => new Image(), []);
|
|
||||||
img.onload = useCallback(
|
|
||||||
(event: Event) => {
|
|
||||||
setHasLoaded(true);
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const ctx = canvasRef.current.getContext("2d");
|
|
||||||
ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight);
|
|
||||||
}
|
|
||||||
onload && onload(event);
|
|
||||||
},
|
|
||||||
[img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config || scaledHeight === 0 || !canvasRef.current) {
|
if (!config || !imgRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${
|
|
||||||
searchParams ? `&${searchParams}` : ""
|
imgRef.current.src = `${apiHost}api/${name}/latest.jpg${
|
||||||
|
searchParams ? `?${searchParams}` : ""
|
||||||
}`;
|
}`;
|
||||||
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
|
}, [apiHost, name, imgRef, searchParams, config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -92,12 +41,16 @@ export default function CameraImage({
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{enabled ? (
|
{enabled ? (
|
||||||
<canvas
|
<img
|
||||||
className="rounded-2xl"
|
ref={imgRef}
|
||||||
data-testid="cameraimage-canvas"
|
className="object-contain rounded-2xl"
|
||||||
height={scaledHeight}
|
onLoad={() => {
|
||||||
ref={canvasRef}
|
setHasLoaded(true);
|
||||||
width={scaledWidth}
|
|
||||||
|
if (onload) {
|
||||||
|
onload();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center pt-6">
|
<div className="text-center pt-6">
|
||||||
@ -105,10 +58,7 @@ export default function CameraImage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!hasLoaded && enabled ? (
|
{!hasLoaded && enabled ? (
|
||||||
<div
|
<div className="absolute left-0 right-0 top-0 bottom-0 flex justify-center items-center">
|
||||||
className="absolute inset-0 flex justify-center"
|
|
||||||
style={{ height: `${scaledHeight}px` }}
|
|
||||||
>
|
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { AspectRatio } from "../ui/aspect-ratio";
|
|
||||||
import CameraImage from "./CameraImage";
|
|
||||||
import { LuEar } from "react-icons/lu";
|
|
||||||
import { CameraConfig } from "@/types/frigateConfig";
|
|
||||||
import { TbUserScan } from "react-icons/tb";
|
|
||||||
import { MdLeakAdd } from "react-icons/md";
|
|
||||||
import {
|
|
||||||
useAudioActivity,
|
|
||||||
useFrigateEvents,
|
|
||||||
useMotionActivity,
|
|
||||||
} from "@/api/ws";
|
|
||||||
|
|
||||||
type DynamicCameraImageProps = {
|
|
||||||
camera: CameraConfig;
|
|
||||||
aspect: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const INTERVAL_INACTIVE_MS = 60000; // refresh once a minute
|
|
||||||
const INTERVAL_ACTIVE_MS = 1000; // refresh once a second
|
|
||||||
|
|
||||||
export default function DynamicCameraImage({
|
|
||||||
camera,
|
|
||||||
aspect,
|
|
||||||
}: DynamicCameraImageProps) {
|
|
||||||
const [key, setKey] = useState(Date.now());
|
|
||||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [activeObjects, setActiveObjects] = useState<string[]>([]);
|
|
||||||
const hasActiveObjects = useMemo(
|
|
||||||
() => activeObjects.length > 0,
|
|
||||||
[activeObjects]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { payload: detectingMotion } = useMotionActivity(camera.name);
|
|
||||||
const { payload: event } = useFrigateEvents();
|
|
||||||
const { payload: audioRms } = useAudioActivity(camera.name);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.after.camera != camera.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == "end") {
|
|
||||||
const eventIndex = activeObjects.indexOf(event.after.id);
|
|
||||||
|
|
||||||
if (eventIndex != -1) {
|
|
||||||
const newActiveObjects = [...activeObjects];
|
|
||||||
newActiveObjects.splice(eventIndex, 1);
|
|
||||||
setActiveObjects(newActiveObjects);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!event.after.stationary) {
|
|
||||||
const eventIndex = activeObjects.indexOf(event.after.id);
|
|
||||||
|
|
||||||
if (eventIndex == -1) {
|
|
||||||
const newActiveObjects = [...activeObjects, event.after.id];
|
|
||||||
setActiveObjects(newActiveObjects);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
setKey(Date.now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [event, activeObjects]);
|
|
||||||
|
|
||||||
const handleLoad = useCallback(() => {
|
|
||||||
const loadTime = Date.now() - key;
|
|
||||||
const loadInterval = hasActiveObjects
|
|
||||||
? INTERVAL_ACTIVE_MS
|
|
||||||
: INTERVAL_INACTIVE_MS;
|
|
||||||
|
|
||||||
const tId = setTimeout(
|
|
||||||
() => {
|
|
||||||
setKey(Date.now());
|
|
||||||
},
|
|
||||||
loadTime > loadInterval ? 1 : loadInterval
|
|
||||||
);
|
|
||||||
setTimeoutId(tId);
|
|
||||||
}, [key]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AspectRatio
|
|
||||||
ratio={aspect}
|
|
||||||
className="bg-black flex justify-center items-center relative"
|
|
||||||
>
|
|
||||||
<CameraImage
|
|
||||||
camera={camera.name}
|
|
||||||
fitAspect={aspect}
|
|
||||||
searchParams={`cache=${key}`}
|
|
||||||
onload={handleLoad}
|
|
||||||
/>
|
|
||||||
<div className="flex absolute right-0 bottom-0 bg-black bg-opacity-20 rounded p-1">
|
|
||||||
<MdLeakAdd
|
|
||||||
className={`${
|
|
||||||
detectingMotion == "ON" ? "text-motion" : "text-gray-600"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<TbUserScan
|
|
||||||
className={`${
|
|
||||||
activeObjects.length > 0 ? "text-object" : "text-gray-600"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{camera.audio.enabled_in_config && (
|
|
||||||
<LuEar
|
|
||||||
className={`${
|
|
||||||
audioRms >= camera.audio.min_volume
|
|
||||||
? "text-audio"
|
|
||||||
: "text-gray-600"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AspectRatio>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
117
web/src/components/camera/ResizingCameraImage.tsx
Normal file
117
web/src/components/camera/ResizingCameraImage.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { useApiHost } from "@/api";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import ActivityIndicator from "../ui/activity-indicator";
|
||||||
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
|
||||||
|
type CameraImageProps = {
|
||||||
|
className?: string;
|
||||||
|
camera: string;
|
||||||
|
onload?: (event: Event) => void;
|
||||||
|
searchParams?: {};
|
||||||
|
stretch?: boolean; // stretch to fit width
|
||||||
|
fitAspect?: number; // shrink to fit height
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CameraImage({
|
||||||
|
className,
|
||||||
|
camera,
|
||||||
|
onload,
|
||||||
|
searchParams = "",
|
||||||
|
stretch = false,
|
||||||
|
fitAspect,
|
||||||
|
}: CameraImageProps) {
|
||||||
|
const { data: config } = useSWR("config");
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
|
useResizeObserver(containerRef);
|
||||||
|
|
||||||
|
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
|
||||||
|
// https://github.com/blakeblackshear/frigate/issues/1657
|
||||||
|
let scrollBarWidth = 0;
|
||||||
|
if (window.innerWidth && document.body.offsetWidth) {
|
||||||
|
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
|
||||||
|
}
|
||||||
|
const availableWidth = scrollBarWidth
|
||||||
|
? containerWidth + scrollBarWidth
|
||||||
|
: containerWidth;
|
||||||
|
|
||||||
|
const { name } = config ? config.cameras[camera] : "";
|
||||||
|
const enabled = config ? config.cameras[camera].enabled : "True";
|
||||||
|
const { width, height } = config
|
||||||
|
? config.cameras[camera].detect
|
||||||
|
: { width: 1, height: 1 };
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
|
||||||
|
const scaledHeight = useMemo(() => {
|
||||||
|
const scaledHeight =
|
||||||
|
aspectRatio < (fitAspect ?? 0)
|
||||||
|
? Math.floor(containerHeight)
|
||||||
|
: Math.floor(availableWidth / aspectRatio);
|
||||||
|
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
|
||||||
|
|
||||||
|
if (finalHeight > 0) {
|
||||||
|
return finalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}, [availableWidth, aspectRatio, height, stretch]);
|
||||||
|
const scaledWidth = useMemo(
|
||||||
|
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
|
||||||
|
[scaledHeight, aspectRatio, scrollBarWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const img = useMemo(() => new Image(), []);
|
||||||
|
img.onload = useCallback(
|
||||||
|
(event: Event) => {
|
||||||
|
setHasLoaded(true);
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const ctx = canvasRef.current.getContext("2d");
|
||||||
|
ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight);
|
||||||
|
}
|
||||||
|
onload && onload(event);
|
||||||
|
},
|
||||||
|
[img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config || scaledHeight === 0 || !canvasRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${
|
||||||
|
searchParams ? `&${searchParams}` : ""
|
||||||
|
}`;
|
||||||
|
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-full flex justify-center ${className}`}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{enabled ? (
|
||||||
|
<canvas
|
||||||
|
className="rounded-2xl"
|
||||||
|
data-testid="cameraimage-canvas"
|
||||||
|
height={scaledHeight}
|
||||||
|
ref={canvasRef}
|
||||||
|
width={scaledWidth}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center pt-6">
|
||||||
|
Camera is disabled in config, no stream or snapshot available!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasLoaded && enabled ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex justify-center"
|
||||||
|
style={{ height: `${scaledHeight}px` }}
|
||||||
|
>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -216,7 +216,9 @@ export default function LivePlayer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Chip className="absolute right-2 top-2 bg-gray-500 bg-gradient-to-br">
|
<Chip className="absolute right-2 top-2 bg-gray-500 bg-gradient-to-br">
|
||||||
|
{cameraConfig.record.enabled && (
|
||||||
<MdCircle className="w-2 h-2 text-danger" />
|
<MdCircle className="w-2 h-2 text-danger" />
|
||||||
|
)}
|
||||||
<div className="ml-1 capitalize text-white text-xs">
|
<div className="ml-1 capitalize text-white text-xs">
|
||||||
{cameraConfig.name.replaceAll("_", " ")}
|
{cameraConfig.name.replaceAll("_", " ")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,176 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
|
||||||
import {
|
|
||||||
useAudioState,
|
|
||||||
useDetectState,
|
|
||||||
useRecordingsState,
|
|
||||||
useSnapshotsState,
|
|
||||||
} from "@/api/ws";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
|
||||||
import Heading from "@/components/ui/heading";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { AiOutlinePicture } from "react-icons/ai";
|
|
||||||
import { FaWalking } from "react-icons/fa";
|
|
||||||
import { LuEar } from "react-icons/lu";
|
|
||||||
import { TbMovie } from "react-icons/tb";
|
|
||||||
import MiniEventCard from "@/components/card/MiniEventCard";
|
|
||||||
import { Event as FrigateEvent } from "@/types/event";
|
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
||||||
import DynamicCameraImage from "@/components/camera/DynamicCameraImage";
|
|
||||||
|
|
||||||
export function Dashboard() {
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
|
||||||
const now = new Date();
|
|
||||||
now.setMinutes(now.getMinutes() - 30, 0, 0);
|
|
||||||
const recentTimestamp = now.getTime() / 1000;
|
|
||||||
const { data: events, mutate: updateEvents } = useSWR<FrigateEvent[]>([
|
|
||||||
"events",
|
|
||||||
{ limit: 10, after: recentTimestamp },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const sortedCameras = useMemo(() => {
|
|
||||||
if (!config) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(config.cameras)
|
|
||||||
.filter((conf) => conf.ui.dashboard)
|
|
||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Heading as="h2">Dashboard</Heading>
|
|
||||||
|
|
||||||
{!config && <ActivityIndicator />}
|
|
||||||
|
|
||||||
{config && (
|
|
||||||
<div>
|
|
||||||
{events && events.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Heading as="h4">Recent Events</Heading>
|
|
||||||
<ScrollArea>
|
|
||||||
<div className="flex">
|
|
||||||
{events.map((event) => {
|
|
||||||
return (
|
|
||||||
<MiniEventCard
|
|
||||||
key={event.id}
|
|
||||||
event={event}
|
|
||||||
onUpdate={() => updateEvents()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ScrollBar orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Heading as="h4">Cameras</Heading>
|
|
||||||
<div className="mt-2 grid gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
|
||||||
{sortedCameras.map((camera) => {
|
|
||||||
return <Camera key={camera.name} camera={camera} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Camera({ camera }: { camera: CameraConfig }) {
|
|
||||||
const { payload: detectValue, send: sendDetect } = useDetectState(
|
|
||||||
camera.name
|
|
||||||
);
|
|
||||||
const { payload: recordValue, send: sendRecord } = useRecordingsState(
|
|
||||||
camera.name
|
|
||||||
);
|
|
||||||
const { payload: snapshotValue, send: sendSnapshot } = useSnapshotsState(
|
|
||||||
camera.name
|
|
||||||
);
|
|
||||||
const { payload: audioValue, send: sendAudio } = useAudioState(camera.name);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card>
|
|
||||||
<a href={`/live/${camera.name}`}>
|
|
||||||
<DynamicCameraImage aspect={16 / 9} camera={camera} />
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="text-lg capitalize p-2">
|
|
||||||
{camera.name.replaceAll("_", " ")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={`${
|
|
||||||
detectValue == "ON" ? "text-primary" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
sendDetect(detectValue == "ON" ? "OFF" : "ON");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaWalking />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={
|
|
||||||
camera.record.enabled_in_config
|
|
||||||
? recordValue == "ON"
|
|
||||||
? "text-primary"
|
|
||||||
: "text-gray-400"
|
|
||||||
: "text-danger"
|
|
||||||
}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
camera.record.enabled_in_config
|
|
||||||
? sendRecord(recordValue == "ON" ? "OFF" : "ON")
|
|
||||||
: {};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TbMovie />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={`${
|
|
||||||
snapshotValue == "ON" ? "text-primary" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
sendSnapshot(detectValue == "ON" ? "OFF" : "ON");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AiOutlinePicture />
|
|
||||||
</Button>
|
|
||||||
{camera.audio.enabled_in_config && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={`${
|
|
||||||
audioValue == "ON" ? "text-primary" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
sendAudio(audioValue == "ON" ? "OFF" : "ON");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuEar />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
Loading…
Reference in New Issue
Block a user