remove old web implementations

This commit is contained in:
Nick Mowen 2023-12-08 07:16:35 -07:00
parent 5c86f6d3f6
commit 23fbbd3c12
5 changed files with 270 additions and 501 deletions

View File

@ -3,111 +3,142 @@ import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer";
import { Card } from "../ui/card"; import { Card } from "../ui/card";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import ActivityIndicator from "../ui/activity-indicator"; import ActivityIndicator from "../ui/activity-indicator";
import { LuCircle, LuClock, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu"; import {
import { IoMdExit } from "react-icons/io" LuCircle,
import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt } from "react-icons/md"; LuClock,
LuPlay,
LuPlayCircle,
LuTruck,
} from "react-icons/lu";
import { IoMdExit } from "react-icons/io";
import {
MdFaceUnlock,
MdOutlineLocationOn,
MdOutlinePictureInPictureAlt,
} from "react-icons/md";
import { HiOutlineVideoCamera } from "react-icons/hi"; import { HiOutlineVideoCamera } from "react-icons/hi";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
type HistoryCardProps = { type HistoryCardProps = {
timeline: Card, timeline: Card;
allPreviews?: Preview[], allPreviews?: Preview[];
} };
export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) { export default function HistoryCard({
const { data: config } = useSWR<FrigateConfig>("config"); allPreviews,
timeline,
}: HistoryCardProps) {
const { data: config } = useSWR<FrigateConfig>("config");
if (!config) { if (!config) {
return <ActivityIndicator /> return <ActivityIndicator />;
} }
return ( return (
<Card className="my-2 mr-2 bg-secondary"> <Card className="my-2 mr-2 bg-secondary">
<PreviewThumbnailPlayer <PreviewThumbnailPlayer
camera={timeline.camera} camera={timeline.camera}
allPreviews={allPreviews || []} allPreviews={allPreviews || []}
startTs={Object.values(timeline.entries)[0].timestamp} startTs={Object.values(timeline.entries)[0].timestamp}
/> />
<div className="p-2"> <div className="p-2">
<div className="text-sm flex"> <div className="text-sm flex">
<LuClock className="h-5 w-5 mr-2 inline" /> <LuClock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(timeline.time, { strftime_fmt: config.ui.time_format == '24hour' ? '%H:%M:%S' : '%I:%M:%S' })} {formatUnixTimestampToDateTime(timeline.time, {
</div> strftime_fmt:
<div className="capitalize text-sm flex align-center mt-1"> config.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S",
<HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" /> })}
{timeline.camera.replaceAll('_', ' ')} </div>
</div> <div className="capitalize text-sm flex align-center mt-1">
<div className="my-2 text-sm font-medium"> <HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" />
Activity: {timeline.camera.replaceAll("_", " ")}
</div> </div>
{Object.entries(timeline.entries).map(([_, entry]) => { <div className="my-2 text-sm font-medium">Activity:</div>
return ( {Object.entries(timeline.entries).map(([_, entry]) => {
<div key={entry.timestamp} className="flex text-xs capitalize my-1 items-center"> return (
{getTimelineIcon(entry)} <div
{getTimelineItemDescription(entry)} key={entry.timestamp}
</div> className="flex text-xs capitalize my-1 items-center"
); >
})} {getTimelineIcon(entry)}
{getTimelineItemDescription(entry)}
</div> </div>
</Card> );
); })}
</div>
</Card>
);
} }
function getTimelineIcon(timelineItem: Timeline) { function getTimelineIcon(timelineItem: Timeline) {
switch (timelineItem.class_type) { switch (timelineItem.class_type) {
case 'visible': case "visible":
return <LuPlay className="w-4 mr-1" />; return <LuPlay className="w-4 mr-1" />;
case 'gone': case "gone":
return <IoMdExit className="w-4 mr-1" />; return <IoMdExit className="w-4 mr-1" />;
case 'active': case "active":
return <LuPlayCircle className="w-4 mr-1" />; return <LuPlayCircle className="w-4 mr-1" />;
case 'stationary': case "stationary":
return <LuCircle className="w-4 mr-1" />; return <LuCircle className="w-4 mr-1" />;
case 'entered_zone': case "entered_zone":
return <MdOutlineLocationOn className="w-4 mr-1" />; return <MdOutlineLocationOn className="w-4 mr-1" />;
case 'attribute': case "attribute":
switch (timelineItem.data.attribute) { switch (timelineItem.data.attribute) {
case 'face': case "face":
return <MdFaceUnlock className="w-4 mr-1" />; return <MdFaceUnlock className="w-4 mr-1" />;
case 'license_plate': case "license_plate":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />; return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
default: default:
return <LuTruck className="w-4 mr-1" />; return <LuTruck className="w-4 mr-1" />;
} }
case 'sub_label': case "sub_label":
switch (timelineItem.data.label) { switch (timelineItem.data.label) {
case 'person': case "person":
return <MdFaceUnlock className="w-4 mr-1" />; return <MdFaceUnlock className="w-4 mr-1" />;
case 'car': case "car":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />; return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
} }
}
} }
}
function getTimelineItemDescription(timelineItem: Timeline) { function getTimelineItemDescription(timelineItem: Timeline) {
const label = ((Array.isArray(timelineItem.data.sub_label) ? timelineItem.data.sub_label[0] : timelineItem.data.sub_label) || timelineItem.data.label).replaceAll('_', ' '); const label = (
(Array.isArray(timelineItem.data.sub_label)
? timelineItem.data.sub_label[0]
: timelineItem.data.sub_label) || timelineItem.data.label
).replaceAll("_", " ");
switch (timelineItem.class_type) { switch (timelineItem.class_type) {
case 'visible': case "visible":
return `${label} detected`; return `${label} detected`;
case 'entered_zone': case "entered_zone":
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`; return `${label} entered ${timelineItem.data.zones
case 'active': .join(" and ")
return `${label} became active`; .replaceAll("_", " ")}`;
case 'stationary': case "active":
return `${label} became stationary`; return `${label} became active`;
case 'attribute': { case "stationary":
let title = ''; return `${label} became stationary`;
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') { case "attribute": {
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`; let title = "";
} else { if (
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`; timelineItem.data.attribute == "face" ||
} timelineItem.data.attribute == "license_plate"
return title; ) {
title = `${timelineItem.data.attribute.replaceAll(
"_",
" "
)} detected for ${label}`;
} else {
title = `${
timelineItem.data.sub_label
} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`;
} }
case 'sub_label': return title;
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case 'gone':
return `${label} left`;
} }
} case "sub_label":
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case "gone":
return `${label} left`;
}
}

View File

@ -5,88 +5,106 @@ import Heading from "@/components/ui/heading";
import ActivityIndicator from "@/components/ui/activity-indicator"; import ActivityIndicator from "@/components/ui/activity-indicator";
import HistoryCard from "@/components/card/HistoryCard"; import HistoryCard from "@/components/card/HistoryCard";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
function History() { function History() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]); const timezone = useMemo(
const { data: hourlyTimeline } = useSWR<HourlyTimeline>(['timeline/hourly', { timezone }]); () =>
const { data: allPreviews } = useSWR<Preview[]>(`preview/all/start/${hourlyTimeline?.start || 0}/end/${hourlyTimeline?.end || 0}`, { revalidateOnFocus: false }); config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
[config]
);
const { data: hourlyTimeline } = useSWR<HourlyTimeline>([
"timeline/hourly",
{ timezone },
]);
const { data: allPreviews } = useSWR<Preview[]>(
`preview/all/start/${hourlyTimeline?.start || 0}/end/${
hourlyTimeline?.end || 0
}`,
{ revalidateOnFocus: false }
);
const [detailLevel, setDetailLevel] = useState<'normal' | 'extra' | 'full'>('normal'); const [detailLevel, setDetailLevel] = useState<"normal" | "extra" | "full">(
"normal"
);
const timelineCards: CardsData | never[] = useMemo(() => { const timelineCards: CardsData | never[] = useMemo(() => {
if (!hourlyTimeline) { if (!hourlyTimeline) {
return []; return [];
} }
const cards: CardsData = {}; const cards: CardsData = {};
Object.keys(hourlyTimeline["hours"]) Object.keys(hourlyTimeline["hours"])
.reverse() .reverse()
.forEach((hour) => { .forEach((hour) => {
const day = new Date(parseInt(hour) * 1000); const day = new Date(parseInt(hour) * 1000);
day.setHours(0, 0, 0, 0); day.setHours(0, 0, 0, 0);
const dayKey = (day.getTime() / 1000).toString(); const dayKey = (day.getTime() / 1000).toString();
const source_to_types: {[key: string]: string[]} = {}; const source_to_types: { [key: string]: string[] } = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => { Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000); const time = new Date(i.timestamp * 1000);
time.setSeconds(0); time.setSeconds(0);
time.setMilliseconds(0); time.setMilliseconds(0);
const key = `${i.source_id}-${time.getMinutes()}`; const key = `${i.source_id}-${time.getMinutes()}`;
if (key in source_to_types) { if (key in source_to_types) {
source_to_types[key].push(i.class_type); source_to_types[key].push(i.class_type);
} else { } else {
source_to_types[key] = [i.class_type]; source_to_types[key] = [i.class_type];
}
});
if (!Object.keys(cards).includes(dayKey)) {
cards[dayKey] = {};
} }
cards[dayKey][hour] = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == 'normal') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
) {
add = false;
}
} else if (detailLevel == 'extra') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
i.class_type in ['attribute', 'gone', 'visible']
) {
add = false;
}
}
if (add) {
if (key in cards[dayKey][hour]) {
cards[dayKey][hour][key].entries.push(i);
} else {
cards[dayKey][hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
}); });
return cards; if (!Object.keys(cards).includes(dayKey)) {
cards[dayKey] = {};
}
cards[dayKey][hour] = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == "normal") {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
1 &&
["active", "attribute", "gone", "stationary", "visible"].includes(
i.class_type
)
) {
add = false;
}
} else if (detailLevel == "extra") {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
1 &&
i.class_type in ["attribute", "gone", "visible"]
) {
add = false;
}
}
if (add) {
if (key in cards[dayKey][hour]) {
cards[dayKey][hour][key].entries.push(i);
} else {
cards[dayKey][hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
});
return cards;
}, [detailLevel, hourlyTimeline]); }, [detailLevel, hourlyTimeline]);
if (!config || !timelineCards) { if (!config || !timelineCards) {
@ -94,43 +112,59 @@ function History() {
} }
return ( return (
<> <>
<Heading as="h2">Review</Heading> <Heading as="h2">Review</Heading>
<div className="text-xs mb-4">Dates and times are based on the timezone {timezone}</div> <div className="text-xs mb-4">
Dates and times are based on the timezone {timezone}
</div>
<div>
{Object.entries(timelineCards)
.reverse()
.map(([day, timelineDay]) => {
return (
<div key={day}>
<Heading as="h3">
{formatUnixTimestampToDateTime(parseInt(day), {
strftime_fmt: "%A %b %d",
})}
</Heading>
{Object.entries(timelineDay).map(([hour, timelineHour]) => {
if (Object.values(timelineHour).length == 0) {
return <></>;
}
<div>
{Object.entries(timelineCards).reverse().map(([day, timelineDay]) => {
return ( return (
<div key={day}> <div key={hour}>
<Heading as="h3"> <Heading as="h4">
{formatUnixTimestampToDateTime(parseInt(day), { strftime_fmt: '%A %b %d' })} {formatUnixTimestampToDateTime(parseInt(hour), {
</Heading> strftime_fmt: "%I:00",
{Object.entries(timelineDay).map(([hour, timelineHour]) => { })}
if (Object.values(timelineHour).length == 0) { </Heading>
return <></>; <ScrollArea>
} <div className="flex">
{Object.entries(timelineHour).map(
([key, timeline]) => {
return ( return (
<div key={hour}> <HistoryCard
<Heading as="h4"> key={key}
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })} timeline={timeline}
</Heading> allPreviews={allPreviews}
<ScrollArea> />
<div className="flex">
{Object.entries(timelineHour).map(([key, timeline]) => {
return <HistoryCard key={key} timeline={timeline} allPreviews={allPreviews} />
})}
</div>
<ScrollBar className="m-2" orientation="horizontal" />
</ScrollArea>
</div>
); );
})} }
</div> )}
</div>
<ScrollBar className="m-2" orientation="horizontal" />
</ScrollArea>
</div>
); );
})} })}
</div> </div>
</> );
})}
</div>
</>
); );
} }

View File

@ -1,83 +0,0 @@
import { h } from 'preact';
import useSWR from 'swr';
import ActivityIndicator from './ActivityIndicator';
import VideoPlayer from './VideoPlayer';
import { useCallback } from 'preact/hooks';
import { useMemo } from 'react';
import { useApiHost } from '../api';
export default function PreviewPlayer({ camera, allPreviews, startTs, mode }) {
const { data: config } = useSWR('config');
const apiHost = useApiHost();
const relevantPreview = useMemo(() => {
return Object.values(allPreviews || []).find(
(preview) => preview.camera == camera && preview.start < startTs && preview.end > startTs
);
}, [allPreviews, camera, startTs]);
const onHover = useCallback(
(isHovered) => {
if (isHovered) {
this.player.play();
} else {
this.player.pause();
this.player.currentTime(startTs - relevantPreview.start);
}
},
[relevantPreview, startTs]
);
if (!relevantPreview) {
return (
<img className={getThumbWidth(camera, config)} src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`} />
);
}
return (
<div
className={mode == 'thumbnail' ? getThumbWidth(camera, config) : ''}
onMouseEnter={() => onHover(true)}
onMouseLeave={() => onHover(false)}
>
<VideoPlayer
tag={startTs}
options={{
preload: 'auto',
autoplay: false,
controls: false,
muted: true,
loadingSpinner: false,
sources: [
{
src: `${relevantPreview.src}`,
type: 'video/mp4',
},
],
}}
seekOptions={{}}
onReady={(player) => {
this.player = player;
this.player.playbackRate(8);
this.player.currentTime(startTs - relevantPreview.start);
}}
onDispose={() => {
this.player = null;
}}
/>
</div>
);
}
function getThumbWidth(camera, config) {
const detect = config.cameras[camera].detect;
if (detect.width / detect.height > 2) {
return 'w-[320px]';
}
if (detect.width / detect.height < 1.4) {
return 'w-[200px]';
}
return 'w-[240px]';
}

View File

@ -1,222 +0,0 @@
import Heading from '../components/Heading';
import { useMemo, useState } from 'preact/hooks';
import useSWR from 'swr';
import ActivityIndicator from '../components/ActivityIndicator';
import PreviewPlayer from '../components/PreviewPlayer';
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
import { Clock } from '../icons/Clock';
import { Camera } from '../icons/Camera';
import ActiveObjectIcon from '../icons/ActiveObject';
import PlayIcon from '../icons/Play';
import ExitIcon from '../icons/Exit';
import StationaryObjectIcon from '../icons/StationaryObject';
import FaceIcon from '../icons/Face';
import LicensePlateIcon from '../icons/LicensePlate';
import DeliveryTruckIcon from '../icons/DeliveryTruck';
import ZoneIcon from '../icons/Zone';
export default function Export() {
const { data: config } = useSWR('config');
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]);
const { data: hourlyTimeline } = useSWR(['timeline/hourly', { timezone }]);
const { data: allPreviews } = useSWR([
`preview/all/start/${Object.keys(hourlyTimeline || [0])[0]}/end/${Object.keys(hourlyTimeline || [0]).slice(-1)[0]}`,
]);
// detail levels can be normal, extra, full
const [detailLevel, setDetailLevel] = useState('normal');
const timelineCards = useMemo(() => {
if (!hourlyTimeline) {
return [];
}
const cards = {};
Object.keys(hourlyTimeline)
.reverse()
.forEach((hour) => {
const source_to_types = {};
Object.values(hourlyTimeline[hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.source_id}-${time.getMinutes()}`;
if (key in source_to_types) {
source_to_types[key].push(i.class_type);
} else {
source_to_types[key] = [i.class_type];
}
});
cards[hour] = {};
Object.values(hourlyTimeline[hour]).forEach((i) => {
const time = new Date(i.timestamp * 1000);
time.setSeconds(0);
time.setMilliseconds(0);
const key = `${i.camera}-${time.getMinutes()}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
if (detailLevel == 'normal') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
) {
add = false;
}
} else if (detailLevel == 'extra') {
if (
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
i.class_type in ['attribute', 'gone', 'visible']
) {
add = false;
}
}
if (add) {
if (key in cards[hour]) {
cards[hour][key].entries.push(i);
} else {
cards[hour][key] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
};
}
}
});
});
return cards;
}, [detailLevel, hourlyTimeline]);
if (!timelineCards) {
return <ActivityIndicator />;
}
return (
<div className="space-y-4 p-2 px-4">
<Heading>Review</Heading>
<div className="text-xs">Dates and times are based on the timezone {timezone}</div>
<div>
{Object.entries(timelineCards).map(([hour, timelineHour]) => {
return (
<div key={hour}>
<Heading size="md">
{formatUnixTimestampToDateTime(hour, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}
</Heading>
<div className="flex overflow-auto">
{Object.entries(timelineHour).map(([key, timeline]) => {
return (
<div
key={key}
className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow my-2 mr-2"
>
<PreviewPlayer
camera={timeline.camera}
allPreviews={allPreviews}
startTs={Object.values(timeline.entries)[0].timestamp}
mode="thumbnail"
/>
<div className="p-2">
<div className="text-sm flex">
<Clock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(timeline.time, { ...config.ui })}
</div>
<div className="capitalize text-sm flex align-center mt-1">
<Camera className="h-5 w-5 mr-2 inline" />
{timeline.camera.replaceAll('_', ' ')}
</div>
<Heading size="xs" className="my-2">
Activity:
</Heading>
{Object.entries(timeline.entries).map(([_, entry]) => {
return (
<div key={entry.timestamp} className="flex text-xs my-1">
{getTimelineIcon(entry)}
{getTimelineItemDescription(config, entry)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
}
function getTimelineIcon(timelineItem) {
switch (timelineItem.class_type) {
case 'visible':
return <PlayIcon className="w-4 mr-1" />;
case 'gone':
return <ExitIcon className="w-4 mr-1" />;
case 'active':
return <ActiveObjectIcon className="w-4 mr-1" />;
case 'stationary':
return <StationaryObjectIcon className="w-4 mr-1" />;
case 'entered_zone':
return <ZoneIcon className="w-4 mr-1" />;
case 'attribute':
switch (timelineItem.data.attribute) {
case 'face':
return <FaceIcon className="w-4 mr-1" />;
case 'license_plate':
return <LicensePlateIcon className="w-4 mr-1" />;
default:
return <DeliveryTruckIcon className="w-4 mr-1" />;
}
case 'sub_label':
switch (timelineItem.data.label) {
case 'person':
return <FaceIcon className="w-4 mr-1" />;
case 'car':
return <LicensePlateIcon className="w-4 mr-1" />;
}
}
}
function getTimelineItemDescription(config, timelineItem) {
const label = (timelineItem.data.sub_label || timelineItem.data.label).replaceAll('_', ' ');
switch (timelineItem.class_type) {
case 'visible':
return `${label} detected`;
case 'entered_zone':
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`;
case 'active':
return `${label} became active`;
case 'stationary':
return `${label} became stationary`;
case 'attribute': {
let title = '';
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`;
} else {
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`;
}
return title;
}
case 'sub_label':
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case 'gone':
return `${label} left`;
}
}

View File

@ -19,7 +19,16 @@ export default defineConfig({
}, },
'/exports': { '/exports': {
target: 'http://localhost:5000' target: 'http://localhost:5000'
} },
'/ws': {
target: 'ws://localhost:5000',
ws: true,
},
'/live': {
target: 'ws://localhost:5000',
changeOrigin: true,
ws: true,
},
} }
}, },
plugins: [ plugins: [