forked from Github/frigate
* Ensure viewport is always full screen * Protect against hour with no cards and ensure data is consistent * Reduce grouped up image refreshes * Include current hour and fix scrubbing bugginess * Scroll initially selected timeline in to view * Expand timelne class type * Use poster image for preview on video player instead of using separate image view * Fix available streaming modes * Incrase timing for grouping timline items * Fix audio activity listener * Fix player not switching views correctly * Use player time to convert to timeline time * Update sub labels for previous timeline items * Show mini timeline bar for non selected items * Rewrite desktop timeline to use separate dynamic video player component * Extend improvements to mobile as well * Improve time formatting * Fix scroll * Fix no preview case * Mobile fixes * Audio toggle fixes * More fixes for mobile * Improve scaling of graph motion activity * Add keyboard shortcut hook and support shortcuts for playback page * Fix sizing of dialog * Improve height scaling of dialog * simplify and fix layout system for timeline * Fix timeilne items not working * Implement basic Frigate+ submitting from timeline
177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
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;
|