From 23fbbd3c12e5decf40fa26afef60ba289674138c Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Fri, 8 Dec 2023 07:16:35 -0700 Subject: [PATCH] remove old web implementations --- web-new/src/components/card/HistoryCard.tsx | 213 +++++++++-------- web-new/src/pages/History.tsx | 242 +++++++++++--------- web/src/components/PreviewPlayer.jsx | 83 ------- web/src/routes/Review.jsx | 222 ------------------ web/vite.config.ts | 11 +- 5 files changed, 270 insertions(+), 501 deletions(-) delete mode 100644 web/src/components/PreviewPlayer.jsx delete mode 100644 web/src/routes/Review.jsx diff --git a/web-new/src/components/card/HistoryCard.tsx b/web-new/src/components/card/HistoryCard.tsx index 65a3c4faf..98c826435 100644 --- a/web-new/src/components/card/HistoryCard.tsx +++ b/web-new/src/components/card/HistoryCard.tsx @@ -3,111 +3,142 @@ import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer"; import { Card } from "../ui/card"; import { FrigateConfig } from "@/types/frigateConfig"; import ActivityIndicator from "../ui/activity-indicator"; -import { LuCircle, LuClock, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu"; -import { IoMdExit } from "react-icons/io" -import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt } from "react-icons/md"; +import { + LuCircle, + 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 { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; type HistoryCardProps = { - timeline: Card, - allPreviews?: Preview[], -} + timeline: Card; + allPreviews?: Preview[]; +}; -export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) { - const { data: config } = useSWR("config"); +export default function HistoryCard({ + allPreviews, + timeline, +}: HistoryCardProps) { + const { data: config } = useSWR("config"); - if (!config) { - return - } + if (!config) { + return ; + } - return ( - - -
-
- - {formatUnixTimestampToDateTime(timeline.time, { strftime_fmt: config.ui.time_format == '24hour' ? '%H:%M:%S' : '%I:%M:%S' })} -
-
- - {timeline.camera.replaceAll('_', ' ')} -
-
- Activity: -
- {Object.entries(timeline.entries).map(([_, entry]) => { - return ( -
- {getTimelineIcon(entry)} - {getTimelineItemDescription(entry)} -
- ); - })} + return ( + + +
+
+ + {formatUnixTimestampToDateTime(timeline.time, { + strftime_fmt: + config.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S", + })} +
+
+ + {timeline.camera.replaceAll("_", " ")} +
+
Activity:
+ {Object.entries(timeline.entries).map(([_, entry]) => { + return ( +
+ {getTimelineIcon(entry)} + {getTimelineItemDescription(entry)}
- - ); + ); + })} +
+
+ ); } function getTimelineIcon(timelineItem: Timeline) { - switch (timelineItem.class_type) { - case 'visible': - return ; - case 'gone': - return ; - case 'active': - return ; - case 'stationary': - return ; - case 'entered_zone': - return ; - case 'attribute': - switch (timelineItem.data.attribute) { - case 'face': - return ; - case 'license_plate': - return ; - default: - return ; - } - case 'sub_label': - switch (timelineItem.data.label) { - case 'person': - return ; - case 'car': - return ; - } - } + switch (timelineItem.class_type) { + case "visible": + return ; + case "gone": + return ; + case "active": + return ; + case "stationary": + return ; + case "entered_zone": + return ; + case "attribute": + switch (timelineItem.data.attribute) { + case "face": + return ; + case "license_plate": + return ; + default: + return ; + } + case "sub_label": + switch (timelineItem.data.label) { + case "person": + return ; + case "car": + return ; + } } +} 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) { - 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; + 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("_", " ")}`; } - case 'sub_label': - return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`; - case 'gone': - return `${label} left`; + return title; } - } \ No newline at end of file + case "sub_label": + return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`; + case "gone": + return `${label} left`; + } +} diff --git a/web-new/src/pages/History.tsx b/web-new/src/pages/History.tsx index 25914cb56..9b98d8b52 100644 --- a/web-new/src/pages/History.tsx +++ b/web-new/src/pages/History.tsx @@ -5,88 +5,106 @@ import Heading from "@/components/ui/heading"; import ActivityIndicator from "@/components/ui/activity-indicator"; import HistoryCard from "@/components/card/HistoryCard"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; function History() { 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/${hourlyTimeline?.start || 0}/end/${hourlyTimeline?.end || 0}`, { revalidateOnFocus: false }); + 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/${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(() => { - if (!hourlyTimeline) { - return []; - } + if (!hourlyTimeline) { + return []; + } - const cards: CardsData = {}; - Object.keys(hourlyTimeline["hours"]) - .reverse() - .forEach((hour) => { - const day = new Date(parseInt(hour) * 1000); - day.setHours(0, 0, 0, 0); - const dayKey = (day.getTime() / 1000).toString(); - const source_to_types: {[key: string]: string[]} = {}; - Object.values(hourlyTimeline["hours"][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]; - } - }); - - if (!Object.keys(cards).includes(dayKey)) { - cards[dayKey] = {}; + const cards: CardsData = {}; + Object.keys(hourlyTimeline["hours"]) + .reverse() + .forEach((hour) => { + const day = new Date(parseInt(hour) * 1000); + day.setHours(0, 0, 0, 0); + const dayKey = (day.getTime() / 1000).toString(); + const source_to_types: { [key: string]: string[] } = {}; + Object.values(hourlyTimeline["hours"][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[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]); if (!config || !timelineCards) { @@ -94,43 +112,59 @@ function History() { } return ( - <> - Review -
Dates and times are based on the timezone {timezone}
+ <> + Review +
+ Dates and times are based on the timezone {timezone} +
+ +
+ {Object.entries(timelineCards) + .reverse() + .map(([day, timelineDay]) => { + return ( +
+ + {formatUnixTimestampToDateTime(parseInt(day), { + strftime_fmt: "%A %b %d", + })} + + {Object.entries(timelineDay).map(([hour, timelineHour]) => { + if (Object.values(timelineHour).length == 0) { + return <>; + } -
- {Object.entries(timelineCards).reverse().map(([day, timelineDay]) => { return ( -
- - {formatUnixTimestampToDateTime(parseInt(day), { strftime_fmt: '%A %b %d' })} - - {Object.entries(timelineDay).map(([hour, timelineHour]) => { - if (Object.values(timelineHour).length == 0) { - return <>; - } - +
+ + {formatUnixTimestampToDateTime(parseInt(hour), { + strftime_fmt: "%I:00", + })} + + +
+ {Object.entries(timelineHour).map( + ([key, timeline]) => { return ( -
- - {formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })} - - -
- {Object.entries(timelineHour).map(([key, timeline]) => { - return - })} -
- -
-
+ ); - })} -
+ } + )} +
+ + +
); - })} -
- + })} +
+ ); + })} +
+ ); } diff --git a/web/src/components/PreviewPlayer.jsx b/web/src/components/PreviewPlayer.jsx deleted file mode 100644 index b87b24dc0..000000000 --- a/web/src/components/PreviewPlayer.jsx +++ /dev/null @@ -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 ( - - ); - } - - return ( -
onHover(true)} - onMouseLeave={() => onHover(false)} - > - { - this.player = player; - this.player.playbackRate(8); - this.player.currentTime(startTs - relevantPreview.start); - }} - onDispose={() => { - this.player = null; - }} - /> -
- ); -} - -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]'; -} diff --git a/web/src/routes/Review.jsx b/web/src/routes/Review.jsx deleted file mode 100644 index 47c5a79dd..000000000 --- a/web/src/routes/Review.jsx +++ /dev/null @@ -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 ; - } - - return ( -
- Review -
Dates and times are based on the timezone {timezone}
- -
- {Object.entries(timelineCards).map(([hour, timelineHour]) => { - return ( -
- - {formatUnixTimestampToDateTime(hour, { - date_style: 'short', - time_style: 'medium', - time_format: config.ui.time_format, - })} - -
- {Object.entries(timelineHour).map(([key, timeline]) => { - return ( -
- -
-
- - {formatUnixTimestampToDateTime(timeline.time, { ...config.ui })} -
-
- - {timeline.camera.replaceAll('_', ' ')} -
- - Activity: - - {Object.entries(timeline.entries).map(([_, entry]) => { - return ( -
- {getTimelineIcon(entry)} - {getTimelineItemDescription(config, entry)} -
- ); - })} -
-
- ); - })} -
-
- ); - })} -
-
- ); -} - -function getTimelineIcon(timelineItem) { - switch (timelineItem.class_type) { - case 'visible': - return ; - case 'gone': - return ; - case 'active': - return ; - case 'stationary': - return ; - case 'entered_zone': - return ; - case 'attribute': - switch (timelineItem.data.attribute) { - case 'face': - return ; - case 'license_plate': - return ; - default: - return ; - } - case 'sub_label': - switch (timelineItem.data.label) { - case 'person': - return ; - case 'car': - return ; - } - } -} - -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`; - } -} diff --git a/web/vite.config.ts b/web/vite.config.ts index 84d451bce..ee33d491a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -19,7 +19,16 @@ export default defineConfig({ }, '/exports': { target: 'http://localhost:5000' - } + }, + '/ws': { + target: 'ws://localhost:5000', + ws: true, + }, + '/live': { + target: 'ws://localhost:5000', + changeOrigin: true, + ws: true, + }, } }, plugins: [