Files
frigate/web/src/pages/UIPlayground.tsx
T

454 lines
17 KiB
TypeScript
Raw Normal View History

2024-02-26 11:00:53 -07:00
import { useMemo, useRef, useState } from "react";
2023-12-20 18:37:35 -06:00
import Heading from "@/components/ui/heading";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
2024-03-03 10:32:47 -06:00
import ActivityIndicator from "@/components/indicators/activity-indicator";
2024-02-20 17:22:59 -06:00
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
2024-03-05 12:55:44 -07:00
import {
MotionData,
ReviewData,
ReviewSegment,
ReviewSeverity,
} from "@/types/review";
2024-02-22 09:28:05 -06:00
import { Button } from "@/components/ui/button";
2024-03-03 10:32:47 -06:00
import CameraActivityIndicator from "@/components/indicators/CameraActivityIndicator";
2024-03-04 10:42:51 -06:00
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
2024-03-18 15:58:54 -05:00
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useNavigate } from "react-router-dom";
2024-03-20 21:56:15 -05:00
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
2024-03-28 10:03:06 -05:00
import { isMobile } from "react-device-detect";
2024-05-09 08:22:48 -05:00
import IconPicker, { IconElement } from "@/components/icons/IconPicker";
2023-12-20 18:37:35 -06:00
// Color data
const colors = [
"background",
"foreground",
"card",
"card-foreground",
"popover",
"popover-foreground",
"primary",
"primary-foreground",
"secondary",
"secondary-foreground",
"muted",
"muted-foreground",
"accent",
"accent-foreground",
"destructive",
"destructive-foreground",
"border",
"input",
"ring",
];
function ColorSwatch({ name, value }: { name: string; value: string }) {
return (
2024-05-14 10:06:44 -05:00
<div className="mb-2 flex items-center">
2023-12-20 18:37:35 -06:00
<div
2024-05-14 10:06:44 -05:00
className="mr-2 h-10 w-10 border border-gray-300"
2023-12-20 18:37:35 -06:00
style={{ backgroundColor: value }}
></div>
<span>{name}</span>
</div>
);
}
2024-03-05 12:55:44 -07:00
function generateRandomMotionAudioData(): MotionData[] {
2024-03-04 10:42:51 -06:00
const now = new Date();
const endTime = now.getTime() / 1000;
const startTime = endTime - 24 * 60 * 60; // 24 hours ago
const interval = 30; // 30 seconds
const data = [];
for (
let startTimestamp = startTime;
startTimestamp < endTime;
startTimestamp += interval
) {
2024-03-05 12:55:44 -07:00
const motion = Math.floor(Math.random() * 101); // Random number between 0 and 100
const audio = Math.random() * -100; // Random negative value between -100 and 0
2024-03-25 11:19:55 -05:00
const camera = "test_camera";
2024-03-04 10:42:51 -06:00
data.push({
start_time: startTimestamp,
2024-03-05 12:55:44 -07:00
motion,
audio,
2024-03-25 11:19:55 -05:00
camera,
2024-03-04 10:42:51 -06:00
});
}
return data;
}
2024-02-20 17:22:59 -06:00
const generateRandomEvent = (): ReviewSegment => {
2024-02-22 21:15:50 -06:00
const start_time =
2024-03-24 12:39:28 -05:00
Math.floor(Date.now() / 1000) - 60 * 30 - Math.random() * 60 * 60;
2024-02-20 17:22:59 -06:00
const end_time = Math.floor(start_time + Math.random() * 60 * 10);
const severities: ReviewSeverity[] = [
"significant_motion",
"detection",
"alert",
];
const severity = severities[Math.floor(Math.random() * severities.length)];
const has_been_reviewed = Math.random() < 0.2;
const id = new Date(start_time * 1000).toISOString(); // Date string as mock ID
// You need to provide values for camera, thumb_path, and data
const camera = "CameraXYZ";
const thumb_path = "/path/to/thumb";
const data: ReviewData = {
audio: [],
detections: [],
objects: [],
significant_motion_areas: [],
zones: [],
};
return {
id,
start_time,
end_time,
severity,
has_been_reviewed,
camera,
thumb_path,
data,
};
};
2023-12-20 18:37:35 -06:00
function UIPlayground() {
const { data: config } = useSWR<FrigateConfig>("config");
2024-02-20 17:22:59 -06:00
const contentRef = useRef<HTMLDivElement>(null);
2024-06-29 10:02:30 -05:00
const containerRef = useRef<HTMLDivElement>(null);
2024-03-20 21:56:15 -05:00
const reviewTimelineRef = useRef<HTMLDivElement>(null);
2024-02-20 17:22:59 -06:00
const [mockEvents, setMockEvents] = useState<ReviewSegment[]>([]);
2024-03-05 12:55:44 -07:00
const [mockMotionData, setMockMotionData] = useState<MotionData[]>([]);
const [handlebarTime, setHandlebarTime] = useState(
2024-03-07 22:02:29 -06:00
Math.round((Date.now() / 1000 - 15 * 60) / 60) * 60,
);
2023-12-20 18:37:35 -06:00
2024-03-18 15:58:54 -05:00
const [exportStartTime, setExportStartTime] = useState(
Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60,
);
const [exportEndTime, setExportEndTime] = useState(
Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60,
);
const [showExportHandles, setShowExportHandles] = useState(false);
const navigate = useNavigate();
2024-02-20 17:22:59 -06:00
useMemo(() => {
2024-03-20 21:56:15 -05:00
const initialEvents = Array.from({ length: 10 }, generateRandomEvent);
2024-02-20 17:22:59 -06:00
setMockEvents(initialEvents);
2024-03-04 10:42:51 -06:00
setMockMotionData(generateRandomMotionAudioData());
2024-02-20 17:22:59 -06:00
}, []);
2024-02-22 21:15:50 -06:00
// Calculate minimap start and end times based on events
const minimapStartTime = useMemo(() => {
if (mockEvents && mockEvents.length > 0) {
return Math.min(...mockEvents.map((event) => event.start_time));
}
return Math.floor(Date.now() / 1000); // Default to current time if no events
2024-02-28 15:23:56 -07:00
}, [mockEvents]);
2024-02-22 21:15:50 -06:00
const minimapEndTime = useMemo(() => {
if (mockEvents && mockEvents.length > 0) {
return Math.max(
2024-02-28 15:23:56 -07:00
...mockEvents.map((event) => event.end_time ?? event.start_time),
2024-02-22 21:15:50 -06:00
);
}
return Math.floor(Date.now() / 1000); // Default to current time if no events
2024-02-28 15:23:56 -07:00
}, [mockEvents]);
2024-02-22 21:15:50 -06:00
2024-02-22 09:28:05 -06:00
const [zoomLevel, setZoomLevel] = useState(0);
const [zoomSettings, setZoomSettings] = useState({
segmentDuration: 60,
timestampSpread: 15,
});
const possibleZoomLevels = [
{ segmentDuration: 60, timestampSpread: 15 },
{ segmentDuration: 30, timestampSpread: 5 },
{ segmentDuration: 10, timestampSpread: 1 },
];
function handleZoomIn() {
const nextZoomLevel = Math.min(
possibleZoomLevels.length - 1,
2024-02-28 15:23:56 -07:00
zoomLevel + 1,
2024-02-22 09:28:05 -06:00
);
setZoomLevel(nextZoomLevel);
setZoomSettings(possibleZoomLevels[nextZoomLevel]);
}
function handleZoomOut() {
const nextZoomLevel = Math.max(0, zoomLevel - 1);
setZoomLevel(nextZoomLevel);
setZoomSettings(possibleZoomLevels[nextZoomLevel]);
}
2024-02-22 21:15:50 -06:00
const [isDragging, setIsDragging] = useState(false);
const handleDraggingChange = (dragging: boolean) => {
setIsDragging(dragging);
};
2024-03-04 10:42:51 -06:00
const [isEventsReviewTimeline, setIsEventsReviewTimeline] = useState(true);
const birdseyeConfig = config?.birdseye;
2024-03-04 10:42:51 -06:00
2024-05-09 08:22:48 -05:00
const [selectedIcon, setSelectedIcon] = useState<IconElement>();
2023-12-20 18:37:35 -06:00
return (
<>
2024-05-14 10:06:44 -05:00
<div className="h-full w-full">
2024-02-22 21:15:50 -06:00
<div className="flex h-full">
2024-05-14 10:06:44 -05:00
<div className="no-scrollbar mr-5 mt-4 flex-1 content-start gap-2 overflow-y-auto">
2024-02-22 21:15:50 -06:00
<Heading as="h2">UI Playground</Heading>
2023-12-20 18:37:35 -06:00
2024-05-09 08:22:48 -05:00
<IconPicker
selectedIcon={selectedIcon}
setSelectedIcon={setSelectedIcon}
/>
{selectedIcon?.name && (
<p>Selected icon name: {selectedIcon.name}</p>
)}
<Heading as="h4" className="my-5">
2024-02-22 21:15:50 -06:00
Scrubber
2024-02-20 17:22:59 -06:00
</Heading>
<p className="text-small">
2024-02-22 21:15:50 -06:00
Shows the 10 most recent events within the last 4 hours
2024-02-20 17:22:59 -06:00
</p>
2023-12-20 18:37:35 -06:00
2024-02-22 21:15:50 -06:00
{!config && <ActivityIndicator />}
<div ref={contentRef}>
<Heading as="h4" className="my-5">
Timeline
</Heading>
2024-02-23 07:52:54 -06:00
<p className="text-small">
Handlebar timestamp: {handlebarTime} -&nbsp;
{new Date(handlebarTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
month: "short",
day: "2-digit",
second: "2-digit",
})}
</p>
2024-02-22 21:15:50 -06:00
<p className="text-small">
Handlebar is dragging: {isDragging ? "yes" : "no"}
</p>
2024-03-18 15:58:54 -05:00
<p className="text-small">
Export start timestamp: {exportStartTime} -&nbsp;
{new Date(exportStartTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
month: "short",
day: "2-digit",
second: "2-digit",
})}
</p>
<p className="text-small">
Export end timestamp: {exportEndTime} -&nbsp;
{new Date(exportEndTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
month: "short",
day: "2-digit",
second: "2-digit",
})}
</p>
2024-03-04 10:42:51 -06:00
<div className="my-4">
<Heading as="h4">Timeline type</Heading>
<Select
defaultValue={isEventsReviewTimeline ? "event" : "motion"}
onValueChange={(checked) => {
setIsEventsReviewTimeline(checked == "event");
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a timeline" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Timeline Type</SelectLabel>
<SelectItem value="event">Event Review</SelectItem>
<SelectItem value="motion">
Motion/Audio Review
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
2024-05-14 10:06:44 -05:00
<div className="flex items-center justify-start p-2">
2024-03-18 15:58:54 -05:00
<Switch
id="exporthandles"
checked={showExportHandles}
onCheckedChange={() => {
setShowExportHandles(!showExportHandles);
if (showExportHandles) {
setExportEndTime(
Math.round((Date.now() / 1000 - 43 * 60) / 60) * 60,
);
setExportStartTime(
Math.round((Date.now() / 1000 - 45 * 60) / 60) * 60,
);
}
}}
/>
<Label className="ml-2" htmlFor="exporthandles">
Show Export Handles
</Label>
</div>
<div>
<Button
onClick={() => {
navigate("/export", {
state: { start: exportStartTime, end: exportEndTime },
});
}}
disabled={!showExportHandles}
>
Export
</Button>
</div>
2024-05-14 10:06:44 -05:00
<div className="my-4 w-[40px]">
2024-03-03 10:32:47 -06:00
<CameraActivityIndicator />
</div>
2024-02-22 21:15:50 -06:00
<p>
<Button
variant="default"
onClick={handleZoomOut}
disabled={zoomLevel === 0}
>
2024-02-22 21:15:50 -06:00
Zoom Out
</Button>
<Button
onClick={handleZoomIn}
disabled={zoomLevel === possibleZoomLevels.length - 1}
>
Zoom In
</Button>
</p>
2024-06-29 10:02:30 -05:00
<div ref={containerRef} className="">
2024-03-18 15:58:54 -05:00
{birdseyeConfig && (
<BirdseyeLivePlayer
birdseyeConfig={birdseyeConfig}
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
2024-06-29 10:02:30 -05:00
containerRef={containerRef}
2024-03-18 15:58:54 -05:00
/>
)}
</div>
2024-02-22 21:15:50 -06:00
<Heading as="h4" className="my-5">
Color scheme
</Heading>
<p className="text-small">
Colors as set by the current theme. See the{" "}
<a
className="underline"
href="https://ui.shadcn.com/docs/theming"
>
shadcn theming docs
</a>{" "}
for usage.
</p>
<div className="my-5">
{colors.map((color, index) => (
<ColorSwatch
key={index}
name={color}
value={`hsl(var(--${color}))`}
/>
))}
</div>
2024-02-20 17:22:59 -06:00
</div>
</div>
2024-02-22 21:15:50 -06:00
2024-05-14 10:06:44 -05:00
<div className="no-scrollbar w-[55px] overflow-y-auto md:w-[100px]">
2024-03-04 10:42:51 -06:00
{!isEventsReviewTimeline && (
<MotionReviewTimeline
segmentDuration={zoomSettings.segmentDuration} // seconds per segment
timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
2024-03-08 10:13:42 -06:00
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
2024-03-26 16:36:28 -05:00
timelineEnd={Math.floor(Date.now() / 1000) - 4 * 60 * 60} // end of timeline - the later time
2024-03-04 10:42:51 -06:00
showHandlebar // show / hide the handlebar
handlebarTime={handlebarTime} // set the time of the handlebar
setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
onHandlebarDraggingChange={handleDraggingChange} // function for state of handlebar dragging
2024-03-24 12:39:28 -05:00
showMinimap={false} // show / hide the minimap
2024-03-04 10:42:51 -06:00
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
2024-03-18 15:58:54 -05:00
showExportHandles={showExportHandles}
exportStartTime={exportStartTime}
setExportStartTime={setExportStartTime}
exportEndTime={exportEndTime}
setExportEndTime={setExportEndTime}
2024-03-04 10:42:51 -06:00
events={mockEvents} // events, including new has_been_reviewed and severity properties
motion_events={mockMotionData}
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
2024-03-28 10:03:06 -05:00
dense={isMobile} // dense will produce a smaller handlebar and only minute resolution on timestamps
2024-03-04 10:42:51 -06:00
/>
)}
{isEventsReviewTimeline && (
<EventReviewTimeline
segmentDuration={zoomSettings.segmentDuration} // seconds per segment
timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
2024-03-26 16:36:28 -05:00
timelineEnd={Math.floor(Date.now() / 1000) - 4 * 60 * 60} // end of timeline - the later time
2024-03-04 10:42:51 -06:00
showHandlebar // show / hide the handlebar
handlebarTime={handlebarTime} // set the time of the handlebar
setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
onHandlebarDraggingChange={handleDraggingChange} // function for state of handlebar dragging
showMinimap // show / hide the minimap
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
2024-03-18 15:58:54 -05:00
showExportHandles={showExportHandles}
exportStartTime={exportStartTime}
setExportStartTime={setExportStartTime}
exportEndTime={exportEndTime}
setExportEndTime={setExportEndTime}
2024-03-04 10:42:51 -06:00
events={mockEvents} // events, including new has_been_reviewed and severity properties
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
2024-03-21 09:00:04 -05:00
timelineRef={reviewTimelineRef} // save a ref to this timeline to connect with the summary timeline
2024-03-28 10:03:06 -05:00
dense // dense will produce a smaller handlebar and only minute resolution on timestamps
2024-03-04 10:42:51 -06:00
/>
)}
2024-02-22 21:15:50 -06:00
</div>
2024-03-20 21:56:15 -05:00
{isEventsReviewTimeline && (
<div className="w-[10px]">
<SummaryTimeline
2024-03-21 09:00:04 -05:00
reviewTimelineRef={reviewTimelineRef} // the ref to the review timeline
2024-03-20 21:56:15 -05:00
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
2024-03-26 16:36:28 -05:00
timelineEnd={Math.floor(Date.now() / 1000) - 4 * 60 * 60} // end of timeline - the later time
2024-03-20 21:56:15 -05:00
segmentDuration={zoomSettings.segmentDuration}
events={mockEvents}
2024-03-21 09:00:04 -05:00
severityType={"alert"} // show only events of this severity on the summary timeline
2024-03-20 21:56:15 -05:00
/>
</div>
)}
2024-02-20 17:22:59 -06:00
</div>
2023-12-20 18:37:35 -06:00
</div>
</>
);
}
export default UIPlayground;