Show mini timeline bar for non selected items

This commit is contained in:
Nick Mowen 2024-01-23 13:50:32 -07:00
parent fc259133ac
commit 11133cb05a
4 changed files with 276 additions and 83 deletions

View File

@ -391,6 +391,17 @@ def set_sub_label(id):
new_sub_label = json.get("subLabel") new_sub_label = json.get("subLabel")
new_score = json.get("subLabelScore") new_score = json.get("subLabelScore")
if new_sub_label is None:
return make_response(
jsonify(
{
"success": False,
"message": "A sub label must be supplied",
}
),
400,
)
if new_sub_label and len(new_sub_label) > 100: if new_sub_label and len(new_sub_label) > 100:
return make_response( return make_response(
jsonify( jsonify(
@ -416,6 +427,7 @@ def set_sub_label(id):
) )
if not event.end_time: if not event.end_time:
# update tracked object
tracked_obj: TrackedObject = ( tracked_obj: TrackedObject = (
current_app.detected_frames_processor.camera_states[ current_app.detected_frames_processor.camera_states[
event.camera event.camera
@ -425,6 +437,11 @@ def set_sub_label(id):
if tracked_obj: if tracked_obj:
tracked_obj.obj_data["sub_label"] = (new_sub_label, new_score) tracked_obj.obj_data["sub_label"] = (new_sub_label, new_score)
# update timeline items
Timeline.update(
data=Timeline.data.update({"sub_label": (new_sub_label, new_score)})
).where(Timeline.source_id == id).execute()
event.sub_label = new_sub_label event.sub_label = new_sub_label
if new_score: if new_score:

View File

@ -7,7 +7,6 @@ from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import ALL_ATTRIBUTE_LABELS
from frigate.events.maintainer import EventTypeEnum from frigate.events.maintainer import EventTypeEnum
from frigate.models import Timeline from frigate.models import Timeline
from frigate.util.builtin import to_relative_box from frigate.util.builtin import to_relative_box
@ -109,22 +108,6 @@ class TimelineProcessor(threading.Thread):
}, },
} }
# make sure all timeline items have a sub label
if prev_event_data != None and prev_event_data.get(
"sub_label"
) != event_data.get("sub_label"):
# update existing timeline items to have sub label
sub_label = event_data["sub_label"]
if sub_label[0] not in ALL_ATTRIBUTE_LABELS:
if event_id in self.pre_event_cache.keys():
for e in self.pre_event_cache[event_id]:
e[Timeline.data]["sub_label"] = sub_label
else:
Timeline.update(
data=Timeline.data.update({"sub_label": sub_label})
).where(Timeline.source_id == event_id).execute()
if event_type == "start": if event_type == "start":
timeline_entry[Timeline.class_type] = "visible" timeline_entry[Timeline.class_type] = "visible"
save = True save = True

View File

@ -0,0 +1,185 @@
import { FrigateConfig } from "@/types/frigateConfig";
import { GraphDataPoint } from "@/types/graph";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import useSWR from "swr";
type TimelineBarProps = {
startTime: number;
graphData:
| {
objects: number[];
motion: GraphDataPoint[];
}
| undefined;
onClick?: () => void;
};
export default function TimelineBar({
startTime,
graphData,
onClick,
}: TimelineBarProps) {
const { data: config } = useSWR<FrigateConfig>("config");
return (
<div
className="my-1 p-1 w-full h-18 border rounded cursor-pointer hover:bg-secondary hover:bg-opacity-30"
onClick={onClick}
>
{graphData != undefined && (
<div className="relative w-full h-8 flex">
{getHourBlocks().map((idx) => {
return (
<div
key={idx}
className={`h-2 flex-auto ${
(graphData.motion.at(idx)?.y || 0) == 0
? ""
: graphData.objects.includes(idx)
? "bg-object"
: "bg-motion"
}`}
/>
);
})}
<div className="absolute left-0 top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:00" : "%I:00%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[8.3%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:05" : "%I:05%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[16.7%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:10" : "%I:10%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[25%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:15" : "%I:15%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[33.3%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:20" : "%I:20%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[41.7%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:25" : "%I:25%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[50%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:30" : "%I:30%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[58.3%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:35" : "%I:35%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[66.7%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:40" : "%I:40%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[75%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:45" : "%I:45%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[83.3%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:50" : "%I:50%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
<div className="absolute left-[91.7%] top-0 bottom-0 align-bottom border-l border-gray-500">
<div className="absolute ml-1 bottom-0 text-sm text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:55" : "%I:55%P",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
</div>
)}
<div className="text-gray-500">
{formatUnixTimestampToDateTime(startTime, {
strftime_fmt: "%a %d %B",
time_style: "medium",
date_style: "medium",
})}
</div>
</div>
);
}
function getHourBlocks() {
const arr = [];
for (let x = 0; x <= 59; x++) {
arr.push(x);
}
return arr;
}

View File

@ -12,6 +12,7 @@ import TimelineItemCard from "@/components/card/TimelineItemCard";
import { getTimelineHoursForDay } from "@/utils/historyUtil"; import { getTimelineHoursForDay } from "@/utils/historyUtil";
import { GraphDataPoint } from "@/types/graph"; import { GraphDataPoint } from "@/types/graph";
import TimelineGraph from "@/components/graph/TimelineGraph"; import TimelineGraph from "@/components/graph/TimelineGraph";
import TimelineBar from "@/components/bar/TimelineBar";
type DesktopTimelineViewProps = { type DesktopTimelineViewProps = {
timelineData: CardsData; timelineData: CardsData;
@ -361,76 +362,83 @@ export default function DesktopTimelineView({
<div <div
ref={isInitiallySelected ? initialScrollRef : null} ref={isInitiallySelected ? initialScrollRef : null}
key={timeline.range.start} key={timeline.range.start}
className={`relative p-2 ${
isSelected ? "bg-secondary bg-opacity-30 rounded-md" : ""
}`}
> >
<ActivityScrubber {isSelected ? (
items={[]} <div className="p-2 relative bg-secondary bg-opacity-30 rounded-md">
timeBars={ <ActivityScrubber
isSelected items={[]}
? [ timeBars={
{ isSelected
time: new Date(timelineTime * 1000), ? [
id: "playback", {
}, time: new Date(timelineTime * 1000),
] id: "playback",
: [] },
} ]
options={{ : []
snap: null, }
min: new Date(timeline.range.start * 1000), options={{
max: new Date(timeline.range.end * 1000), snap: null,
zoomable: false, min: new Date(timeline.range.start * 1000),
}} max: new Date(timeline.range.end * 1000),
timechangeHandler={(data) => { zoomable: false,
if (!timeline.relevantPreview) { }}
return; timechangeHandler={(data) => {
} if (!timeline.relevantPreview) {
playerRef.current?.pause();
return;
}
const seekTimestamp = data.time.getTime() / 1000; const seekTimestamp = data.time.getTime() / 1000;
const seekTime = const seekTime =
seekTimestamp - timeline.relevantPreview.start; seekTimestamp - timeline.relevantPreview.start;
setPlayerTime(seekTimestamp - timeline.range.start); setPlayerTime(seekTimestamp - timeline.range.start);
setTimeToSeek(Math.round(seekTime)); setTimeToSeek(Math.round(seekTime));
}} }}
timechangedHandler={(data) => { timechangedHandler={(data) => {
setScrubbing(false); setScrubbing(false);
const playbackTime = const playbackTime =
data.time.getTime() / 1000 - timeline.range.start; data.time.getTime() / 1000 - timeline.range.start;
playerRef.current?.currentTime(playbackTime); playerRef.current?.currentTime(playbackTime);
playerRef.current?.play(); playerRef.current?.play();
}} }}
selectHandler={(data) => { selectHandler={(data) => {
if (data.items.length > 0) { if (data.items.length > 0) {
const selected = data.items[0]; const selected = data.items[0];
onSelectItem( onSelectItem(
selectedPlayback.timelineItems.find( selectedPlayback.timelineItems.find(
(timeline) => timeline.timestamp == selected (timeline) => timeline.timestamp == selected
) )
); );
} }
}} }}
doubleClickHandler={() => {
setScrubbing(false);
setSelectedPlayback(timeline);
}}
/>
{isSelected && graphData && (
<div className="absolute left-2 right-2 top-0 h-[84px]">
<TimelineGraph
id={timeline.range.start.toString()}
data={[
{
name: "Motion",
data: graphData.motion,
},
]}
objects={graphData.objects}
start={graphData.motion[0].x.getTime()}
end={graphData.motion.at(-1)!!.x.getTime()}
/> />
{isSelected && graphData && (
<div className="absolute left-2 right-2 top-0 h-[84px]">
<TimelineGraph
id={timeline.range.start.toString()}
data={[
{
name: "Motion",
data: graphData.motion,
},
]}
objects={graphData.objects}
start={graphData.motion[0].x.getTime()}
end={graphData.motion.at(-1)!!.x.getTime()}
/>
</div>
)}
</div> </div>
) : (
<TimelineBar
startTime={timeline.range.start}
graphData={graphData}
onClick={() => {
setScrubbing(false);
setSelectedPlayback(timeline);
}}
/>
)} )}
</div> </div>
); );