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 { useEffect, useRef, useState } from "react";
|
||||||
import { Timeline } from "vis-timeline";
|
import {
|
||||||
import { TimelineOptions } from "vis-timeline/standalone";
|
Timeline as VisTimeline,
|
||||||
|
TimelineGroup,
|
||||||
|
TimelineItem,
|
||||||
|
TimelineOptions,
|
||||||
|
} from "vis-timeline";
|
||||||
|
import type { DataGroup, DataItem, TimelineEvents } from "vis-timeline/types";
|
||||||
import "./scrubber.css";
|
import "./scrubber.css";
|
||||||
|
|
||||||
export type ScrubberItem = {
|
export type TimelineEventsWithMissing =
|
||||||
id: string;
|
| TimelineEvents
|
||||||
content: string;
|
| "dragover"
|
||||||
start: Date;
|
| "markerchange"
|
||||||
end?: Date;
|
| "markerchanged";
|
||||||
type?: "box" | "point";
|
|
||||||
|
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 = {
|
export type TimelineEventsHandlers = Partial<
|
||||||
nodes: ScrubberItem[];
|
Record<TimelineEventHandler, EventHandler>
|
||||||
};
|
>;
|
||||||
|
|
||||||
type ScrubberChartProps = {
|
export type ScrubberItem = TimelineItem;
|
||||||
items: ScrubberItem[];
|
|
||||||
|
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;
|
options?: TimelineOptions;
|
||||||
onSelect?: (props: ScrubberSelectProps) => void;
|
} & TimelineEventsHandlers;
|
||||||
};
|
|
||||||
|
|
||||||
export function ActivityScrubber({
|
function ActivityScrubber({
|
||||||
items,
|
items,
|
||||||
|
groups,
|
||||||
options,
|
options,
|
||||||
onSelect,
|
...eventHandlers
|
||||||
}: ScrubberChartProps) {
|
}: ActivityScrubberProps) {
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const timelineRef = useRef<Timeline | null>(null);
|
const timelineRef = useRef<{ timeline: VisTimeline | null }>({
|
||||||
|
timeline: null,
|
||||||
|
});
|
||||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
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(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
setCurrentTime(Date.now());
|
setCurrentTime(Date.now());
|
||||||
}, 10000);
|
}, 60000); // Update every minute
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
@ -41,54 +118,63 @@ export function ActivityScrubber({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultOptions: TimelineOptions = {
|
const divElement = containerRef.current;
|
||||||
width: "100%",
|
if (!divElement) {
|
||||||
stack: true,
|
return;
|
||||||
showMajorLabels: true,
|
}
|
||||||
showCurrentTime: true,
|
|
||||||
zoomMin: 10 * 1000, // 10 seconds
|
const timelineInstance = new VisTimeline(
|
||||||
end: currentTime,
|
divElement,
|
||||||
max: currentTime,
|
items as DataItem[],
|
||||||
format: {
|
groups as DataGroup[],
|
||||||
minorLabels: {
|
options
|
||||||
minute: "h:mma",
|
);
|
||||||
hour: "ha",
|
|
||||||
},
|
domEvents.forEach((event) => {
|
||||||
},
|
const eventHandler = eventHandlers[`${event}Handler`];
|
||||||
};
|
if (typeof eventHandler === "function") {
|
||||||
|
timelineInstance.on(event, eventHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
timelineRef.current.timeline = timelineInstance;
|
||||||
|
|
||||||
const timelineOptions: TimelineOptions = {
|
const timelineOptions: TimelineOptions = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!timelineRef.current) {
|
timelineInstance.setOptions(timelineOptions);
|
||||||
timelineRef.current = new Timeline(
|
|
||||||
container.current as HTMLDivElement,
|
|
||||||
items,
|
|
||||||
timelineOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateRange = () => {
|
return () => {
|
||||||
timelineRef.current?.setOptions(timelineOptions);
|
timelineInstance.destroy();
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
timelineRef.current.on("rangechanged", updateRange);
|
useEffect(() => {
|
||||||
if (onSelect) {
|
if (!timelineRef.current.timeline) {
|
||||||
timelineRef.current.on("select", onSelect);
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
timelineRef.current?.off("rangechanged", updateRange);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Update existing timeline
|
|
||||||
timelineRef.current.setItems(items);
|
|
||||||
timelineRef.current.setOptions(timelineOptions);
|
|
||||||
}
|
}
|
||||||
}, [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;
|
export default ActivityScrubber;
|
||||||
|
|||||||
@ -14,10 +14,10 @@
|
|||||||
@apply absolute invisible mx-0 px-0;
|
@apply absolute invisible mx-0 px-0;
|
||||||
}
|
}
|
||||||
.vis-time-axis .vis-grid.vis-vertical {
|
.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 {
|
.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 {
|
.vis-time-axis .vis-grid.vis-minor {
|
||||||
@apply border-foreground;
|
@apply border-foreground;
|
||||||
@ -170,16 +170,16 @@
|
|||||||
@apply min-h-0 w-auto;
|
@apply min-h-0 w-auto;
|
||||||
}
|
}
|
||||||
.vis-item {
|
.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 {
|
.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 {
|
.vis-editable.vis-selected {
|
||||||
@apply cursor-move;
|
@apply cursor-move;
|
||||||
}
|
}
|
||||||
.vis-item.vis-point.vis-selected {
|
.vis-item.vis-point.vis-selected {
|
||||||
@apply bg-[#fff785];
|
@apply bg-muted-foreground;
|
||||||
}
|
}
|
||||||
.vis-item.vis-box {
|
.vis-item.vis-box {
|
||||||
@apply text-center rounded-sm border-solid;
|
@apply text-center rounded-sm border-solid;
|
||||||
@ -209,8 +209,7 @@
|
|||||||
@apply inline-block absolute;
|
@apply inline-block absolute;
|
||||||
}
|
}
|
||||||
.vis-item.vis-line {
|
.vis-item.vis-line {
|
||||||
@apply absolute w-0 p-0 border-l;
|
@apply absolute w-0 p-0 border-l border-solid text-muted;
|
||||||
border-left-style: solid;
|
|
||||||
}
|
}
|
||||||
.vis-item .vis-item-content {
|
.vis-item .vis-item-content {
|
||||||
@apply box-border whitespace-nowrap p-[5px];
|
@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 Heading from "@/components/ui/heading";
|
||||||
import {
|
import ActivityScrubber, {
|
||||||
ScrubberSelectProps,
|
|
||||||
ActivityScrubber,
|
|
||||||
ScrubberItem,
|
ScrubberItem,
|
||||||
} from "@/components/scrubber/ActivityScrubber";
|
} from "@/components/scrubber/ActivityScrubber";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||||
|
import { useApiHost } from "@/api";
|
||||||
|
import TimelineScrubber from "@/components/playground/TimelineScrubber";
|
||||||
|
|
||||||
// Color data
|
// Color data
|
||||||
const colors = [
|
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[] {
|
function eventsToScrubberItems(events: Event[]): ScrubberItem[] {
|
||||||
return events.map((event) => {
|
const apiHost = useApiHost();
|
||||||
return {
|
|
||||||
id: event.id,
|
return events.map((event: Event) => ({
|
||||||
content: event.label,
|
id: event.id,
|
||||||
start: new Date(event.start_time * 1000),
|
content: `<div class="flex"><img class="" src="${apiHost}api/events/${event.id}/thumbnail.jpg" /><span>${event.label}</span></div>`,
|
||||||
end: event.end_time ? new Date(event.end_time * 1000) : undefined,
|
start: new Date(event.start_time * 1000),
|
||||||
type: "box",
|
end: event.end_time ? new Date(event.end_time * 1000) : undefined,
|
||||||
};
|
type: "box",
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function UIPlayground() {
|
function UIPlayground() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
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 recentTimestamp = useMemo(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -93,13 +94,23 @@ function UIPlayground() {
|
|||||||
<>
|
<>
|
||||||
<ActivityScrubber
|
<ActivityScrubber
|
||||||
items={eventsToScrubberItems(events)}
|
items={eventsToScrubberItems(events)}
|
||||||
onSelect={onSelect}
|
selectHandler={onSelect}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{config && (
|
||||||
|
<div>
|
||||||
|
{timeline && (
|
||||||
|
<>
|
||||||
|
<TimelineScrubber eventID={timeline} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Heading as="h4" className="my-5">
|
<Heading as="h4" className="my-5">
|
||||||
Color scheme
|
Color scheme
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user