mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 03:35:26 +03:00
timeline scrubber and revamp for all event handlers
This commit is contained in:
parent
9167130896
commit
3c498fc087
46
web/src/components/playground/TimelineScrubber.tsx
Normal file
46
web/src/components/playground/TimelineScrubber.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import useSWR from "swr";
|
||||
import ActivityScrubber, { ScrubberItem } from "../scrubber/ActivityScrubber";
|
||||
|
||||
type TimelineScrubberProps = {
|
||||
eventID: string;
|
||||
};
|
||||
|
||||
function timelineEventsToScrubberItems(events: Timeline[]): ScrubberItem[] {
|
||||
return events.map((event: Timeline, index: number) => ({
|
||||
id: index,
|
||||
content: event.class_type,
|
||||
start: event.timestamp * 1000,
|
||||
type: "box",
|
||||
}));
|
||||
}
|
||||
|
||||
function generateScrubberOptions(events: Timeline[]) {
|
||||
const startTime = events[0].timestamp * 1000 - 10;
|
||||
const endTime = events[events.length - 1].timestamp * 1000 + 10;
|
||||
|
||||
return { start: startTime, end: endTime };
|
||||
}
|
||||
|
||||
function TimelineScrubber({ eventID }: TimelineScrubberProps) {
|
||||
const { data: eventTimeline } = useSWR<Timeline[]>([
|
||||
"timeline",
|
||||
{
|
||||
source_id: eventID,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventTimeline && (
|
||||
<>
|
||||
<ActivityScrubber
|
||||
items={timelineEventsToScrubberItems(eventTimeline)}
|
||||
options={generateScrubberOptions(eventTimeline)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineScrubber;
|
||||
@ -1,39 +1,116 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Timeline } from "vis-timeline";
|
||||
import { TimelineOptions } from "vis-timeline/standalone";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Timeline as VisTimeline,
|
||||
TimelineGroup,
|
||||
TimelineItem,
|
||||
TimelineOptions,
|
||||
} from "vis-timeline";
|
||||
import type { DataGroup, DataItem, TimelineEvents } from "vis-timeline/types";
|
||||
import "./scrubber.css";
|
||||
|
||||
export type ScrubberItem = {
|
||||
id: string;
|
||||
content: string;
|
||||
start: Date;
|
||||
end?: Date;
|
||||
type?: "box" | "point";
|
||||
export type TimelineEventsWithMissing =
|
||||
| TimelineEvents
|
||||
| "dragover"
|
||||
| "markerchange"
|
||||
| "markerchanged";
|
||||
|
||||
export type TimelineEventHandler =
|
||||
| "currentTimeTickHandler"
|
||||
| "clickHandler"
|
||||
| "contextmenuHandler"
|
||||
| "doubleClickHandler"
|
||||
| "dragoverHandler"
|
||||
| "dropHandler"
|
||||
| "mouseOverHandler"
|
||||
| "mouseDownHandler"
|
||||
| "mouseUpHandler"
|
||||
| "mouseMoveHandler"
|
||||
| "groupDraggedHandler"
|
||||
| "changedHandler"
|
||||
| "rangechangeHandler"
|
||||
| "rangechangedHandler"
|
||||
| "selectHandler"
|
||||
| "itemoverHandler"
|
||||
| "itemoutHandler"
|
||||
| "timechangeHandler"
|
||||
| "timechangedHandler"
|
||||
| "markerchangeHandler"
|
||||
| "markerchangedHandler";
|
||||
|
||||
type EventHandler = {
|
||||
(properties: any): void;
|
||||
};
|
||||
|
||||
export type ScrubberSelectProps = {
|
||||
nodes: ScrubberItem[];
|
||||
};
|
||||
export type TimelineEventsHandlers = Partial<
|
||||
Record<TimelineEventHandler, EventHandler>
|
||||
>;
|
||||
|
||||
type ScrubberChartProps = {
|
||||
items: ScrubberItem[];
|
||||
export type ScrubberItem = TimelineItem;
|
||||
|
||||
const domEvents: TimelineEventsWithMissing[] = [
|
||||
"currentTimeTick",
|
||||
"click",
|
||||
"contextmenu",
|
||||
"doubleClick",
|
||||
"dragover",
|
||||
"drop",
|
||||
"mouseOver",
|
||||
"mouseDown",
|
||||
"mouseUp",
|
||||
"mouseMove",
|
||||
"groupDragged",
|
||||
"changed",
|
||||
"rangechange",
|
||||
"rangechanged",
|
||||
"select",
|
||||
"itemover",
|
||||
"itemout",
|
||||
"timechange",
|
||||
"timechanged",
|
||||
"markerchange",
|
||||
"markerchanged",
|
||||
];
|
||||
|
||||
type ActivityScrubberProps = {
|
||||
items: TimelineItem[];
|
||||
groups?: TimelineGroup[];
|
||||
options?: TimelineOptions;
|
||||
onSelect?: (props: ScrubberSelectProps) => void;
|
||||
};
|
||||
} & TimelineEventsHandlers;
|
||||
|
||||
export function ActivityScrubber({
|
||||
function ActivityScrubber({
|
||||
items,
|
||||
groups,
|
||||
options,
|
||||
onSelect,
|
||||
}: ScrubberChartProps) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const timelineRef = useRef<Timeline | null>(null);
|
||||
...eventHandlers
|
||||
}: ActivityScrubberProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const timelineRef = useRef<{ timeline: VisTimeline | null }>({
|
||||
timeline: null,
|
||||
});
|
||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||
|
||||
const defaultOptions: TimelineOptions = {
|
||||
width: "100%",
|
||||
maxHeight: "350px",
|
||||
stack: true,
|
||||
showMajorLabels: true,
|
||||
showCurrentTime: false,
|
||||
zoomMin: 10 * 1000, // 10 seconds
|
||||
// start: new Date(currentTime - 60 * 1 * 60 * 1000), // 1 hour ago
|
||||
end: currentTime,
|
||||
max: currentTime,
|
||||
format: {
|
||||
minorLabels: {
|
||||
minute: "h:mma",
|
||||
hour: "ha",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, 10000);
|
||||
}, 60000); // Update every minute
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
@ -41,54 +118,63 @@ export function ActivityScrubber({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultOptions: TimelineOptions = {
|
||||
width: "100%",
|
||||
stack: true,
|
||||
showMajorLabels: true,
|
||||
showCurrentTime: true,
|
||||
zoomMin: 10 * 1000, // 10 seconds
|
||||
end: currentTime,
|
||||
max: currentTime,
|
||||
format: {
|
||||
minorLabels: {
|
||||
minute: "h:mma",
|
||||
hour: "ha",
|
||||
},
|
||||
},
|
||||
};
|
||||
const divElement = containerRef.current;
|
||||
if (!divElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timelineInstance = new VisTimeline(
|
||||
divElement,
|
||||
items as DataItem[],
|
||||
groups as DataGroup[],
|
||||
options
|
||||
);
|
||||
|
||||
domEvents.forEach((event) => {
|
||||
const eventHandler = eventHandlers[`${event}Handler`];
|
||||
if (typeof eventHandler === "function") {
|
||||
timelineInstance.on(event, eventHandler);
|
||||
}
|
||||
});
|
||||
|
||||
timelineRef.current.timeline = timelineInstance;
|
||||
|
||||
const timelineOptions: TimelineOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (!timelineRef.current) {
|
||||
timelineRef.current = new Timeline(
|
||||
container.current as HTMLDivElement,
|
||||
items,
|
||||
timelineOptions
|
||||
);
|
||||
timelineInstance.setOptions(timelineOptions);
|
||||
|
||||
const updateRange = () => {
|
||||
timelineRef.current?.setOptions(timelineOptions);
|
||||
};
|
||||
return () => {
|
||||
timelineInstance.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
timelineRef.current.on("rangechanged", updateRange);
|
||||
if (onSelect) {
|
||||
timelineRef.current.on("select", onSelect);
|
||||
}
|
||||
|
||||
return () => {
|
||||
timelineRef.current?.off("rangechanged", updateRange);
|
||||
};
|
||||
} else {
|
||||
// Update existing timeline
|
||||
timelineRef.current.setItems(items);
|
||||
timelineRef.current.setOptions(timelineOptions);
|
||||
useEffect(() => {
|
||||
if (!timelineRef.current.timeline) {
|
||||
return;
|
||||
}
|
||||
}, [items, options, currentTime]);
|
||||
|
||||
return <div ref={container}></div>;
|
||||
// If the currentTime updates, adjust the scrubber's end date and max
|
||||
// May not be applicable to all scrubbers, might want to just pass this in
|
||||
// for any scrubbers that we want to dynamically move based on time
|
||||
// const updatedTimeOptions: TimelineOptions = {
|
||||
// end: currentTime,
|
||||
// max: currentTime,
|
||||
// };
|
||||
|
||||
const timelineOptions: TimelineOptions = {
|
||||
...defaultOptions,
|
||||
// ...updatedTimeOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
timelineRef.current.timeline.setOptions(timelineOptions);
|
||||
if (items) timelineRef.current.timeline.setItems(items);
|
||||
}, [items, groups, options, currentTime, eventHandlers]);
|
||||
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
|
||||
export default ActivityScrubber;
|
||||
|
||||
@ -14,10 +14,10 @@
|
||||
@apply absolute invisible mx-0 px-0;
|
||||
}
|
||||
.vis-time-axis .vis-grid.vis-vertical {
|
||||
@apply absolute border-l border-solid;
|
||||
@apply absolute border-l border-dashed border-muted-foreground;
|
||||
}
|
||||
.vis-time-axis .vis-grid.vis-vertical-rtl {
|
||||
@apply absolute border-r border-solid;
|
||||
@apply absolute border-r border-dashed border-muted-foreground;
|
||||
}
|
||||
.vis-time-axis .vis-grid.vis-minor {
|
||||
@apply border-foreground;
|
||||
@ -170,16 +170,16 @@
|
||||
@apply min-h-0 w-auto;
|
||||
}
|
||||
.vis-item {
|
||||
@apply bg-[#d5ddf6] border text-[#1a1a1a] inline-block absolute z-[1] border-[#97b0f8];
|
||||
@apply bg-accent border text-foreground inline-block absolute z-[1] border-border;
|
||||
}
|
||||
.vis-item.vis-selected {
|
||||
@apply bg-[#fff785] z-[2] border-[#ffc200];
|
||||
@apply bg-muted-foreground z-[2] border-muted text-muted;
|
||||
}
|
||||
.vis-editable.vis-selected {
|
||||
@apply cursor-move;
|
||||
}
|
||||
.vis-item.vis-point.vis-selected {
|
||||
@apply bg-[#fff785];
|
||||
@apply bg-muted-foreground;
|
||||
}
|
||||
.vis-item.vis-box {
|
||||
@apply text-center rounded-sm border-solid;
|
||||
@ -209,8 +209,7 @@
|
||||
@apply inline-block absolute;
|
||||
}
|
||||
.vis-item.vis-line {
|
||||
@apply absolute w-0 p-0 border-l;
|
||||
border-left-style: solid;
|
||||
@apply absolute w-0 p-0 border-l border-solid text-muted;
|
||||
}
|
||||
.vis-item .vis-item-content {
|
||||
@apply box-border whitespace-nowrap p-[5px];
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Heading from "@/components/ui/heading";
|
||||
import {
|
||||
ScrubberSelectProps,
|
||||
ActivityScrubber,
|
||||
import ActivityScrubber, {
|
||||
ScrubberItem,
|
||||
} from "@/components/scrubber/ActivityScrubber";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Event } from "@/types/event";
|
||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||
import { useApiHost } from "@/api";
|
||||
import TimelineScrubber from "@/components/playground/TimelineScrubber";
|
||||
|
||||
// Color data
|
||||
const colors = [
|
||||
@ -45,24 +45,25 @@ function ColorSwatch({ name, value }: { name: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function onSelect(props: ScrubberSelectProps) {
|
||||
console.log(props);
|
||||
}
|
||||
|
||||
function eventsToScrubberItems(events: Event[]): ScrubberItem[] {
|
||||
return events.map((event) => {
|
||||
return {
|
||||
id: event.id,
|
||||
content: event.label,
|
||||
start: new Date(event.start_time * 1000),
|
||||
end: event.end_time ? new Date(event.end_time * 1000) : undefined,
|
||||
type: "box",
|
||||
};
|
||||
});
|
||||
const apiHost = useApiHost();
|
||||
|
||||
return events.map((event: Event) => ({
|
||||
id: event.id,
|
||||
content: `<div class="flex"><img class="" src="${apiHost}api/events/${event.id}/thumbnail.jpg" /><span>${event.label}</span></div>`,
|
||||
start: new Date(event.start_time * 1000),
|
||||
end: event.end_time ? new Date(event.end_time * 1000) : undefined,
|
||||
type: "box",
|
||||
}));
|
||||
}
|
||||
|
||||
function UIPlayground() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const [timeline, setTimeline] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSelect = useCallback(({ items }: { items: string[] }) => {
|
||||
setTimeline(items[0]);
|
||||
}, []);
|
||||
|
||||
const recentTimestamp = useMemo(() => {
|
||||
const now = new Date();
|
||||
@ -93,13 +94,23 @@ function UIPlayground() {
|
||||
<>
|
||||
<ActivityScrubber
|
||||
items={eventsToScrubberItems(events)}
|
||||
onSelect={onSelect}
|
||||
selectHandler={onSelect}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config && (
|
||||
<div>
|
||||
{timeline && (
|
||||
<>
|
||||
<TimelineScrubber eventID={timeline} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Heading as="h4" className="my-5">
|
||||
Color scheme
|
||||
</Heading>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user