diff --git a/web-new/src/App.tsx b/web-new/src/App.tsx index 30313f9ec..ea71ee5d6 100644 --- a/web-new/src/App.tsx +++ b/web-new/src/App.tsx @@ -7,7 +7,6 @@ import Header from "@/components/Header"; import Dashboard from "@/pages/Dashboard"; import Live from "@/pages/Live"; import History from "@/pages/History"; -import Review from "@/pages/Review"; import Export from "@/pages/Export"; import Storage from "@/pages/Storage"; import System from "@/pages/System"; @@ -35,7 +34,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/web-new/src/components/Sidebar.tsx b/web-new/src/components/Sidebar.tsx index 33794442f..069f6307c 100644 --- a/web-new/src/components/Sidebar.tsx +++ b/web-new/src/components/Sidebar.tsx @@ -1,6 +1,5 @@ import { IconType } from "react-icons"; import { LuFileUp, LuFilm, LuLayoutDashboard, LuVideo } from "react-icons/lu"; -import { MdOutlinePreview } from "react-icons/md" import { NavLink } from "react-router-dom"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import Logo from "./Logo"; @@ -26,12 +25,6 @@ const navbarLinks = [ }, { id: 4, - icon: MdOutlinePreview, - title: "Review", - url: "/review", - }, - { - id: 5, icon: LuFileUp, title: "Export", url: "/export", diff --git a/web-new/src/components/card/ReviewCard.tsx b/web-new/src/components/card/HistoryCard.tsx similarity index 95% rename from web-new/src/components/card/ReviewCard.tsx rename to web-new/src/components/card/HistoryCard.tsx index 5322df511..6ac420e8c 100644 --- a/web-new/src/components/card/ReviewCard.tsx +++ b/web-new/src/components/card/HistoryCard.tsx @@ -1,7 +1,6 @@ import useSWR from "swr"; -import PreviewPlayer from "../player/PreviewPlayer"; +import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer"; import { Card } from "../ui/card"; -import Heading from "../ui/heading"; import { FrigateConfig } from "@/types/frigateConfig"; import ActivityIndicator from "../ui/activity-indicator"; import { LuCircle, LuClock, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu"; @@ -10,12 +9,12 @@ import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt } from import { HiOutlineVideoCamera } from "react-icons/hi"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; -type ReviewCardProps = { +type HistoryCardProps = { timeline: Card, allPreviews?: Preview[], } -export default function ReviewCard({ allPreviews, timeline }: ReviewCardProps) { +export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) { const { data: config } = useSWR("config"); if (!config) { @@ -24,7 +23,7 @@ export default function ReviewCard({ allPreviews, timeline }: ReviewCardProps) { return ( - (null); const apiHost = useApiHost(); @@ -54,7 +53,7 @@ export default function PreviewPlayer({ camera, allPreviews, startTs, mode }: Pr return (
onHover(true)} onMouseLeave={() => onHover(false)} > diff --git a/web-new/src/pages/History.tsx b/web-new/src/pages/History.tsx index a5ea4ea95..845441cb1 100644 --- a/web-new/src/pages/History.tsx +++ b/web-new/src/pages/History.tsx @@ -1,10 +1,138 @@ +import { useMemo, useState } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; 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";; 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 [detailLevel, setDetailLevel] = useState<'normal' | 'extra' | 'full'>('normal'); + + const timelineCards: CardsData | never[] = useMemo(() => { + 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] = {}; + } + cards[dayKey][hour] = {}; + Object.values(hourlyTimeline["hours"][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[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) { + return ; + } + return ( - <> - History - + <> + 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 <>; + } + + return ( +
+ + {formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })} + + +
+ {Object.entries(timelineHour).map(([key, timeline]) => { + return + })} +
+ +
+
+ ); + })} +
+ ); + })} +
+ ); } diff --git a/web-new/src/pages/Review.tsx b/web-new/src/pages/Review.tsx deleted file mode 100644 index 0f33b0b40..000000000 --- a/web-new/src/pages/Review.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useMemo, useState } from "react"; -import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; -import Heading from "@/components/ui/heading"; -import ActivityIndicator from "@/components/ui/activity-indicator"; -import ReviewCard from "@/components/card/ReviewCard"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; - -export function Review() { - 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 [detailLevel, setDetailLevel] = useState<'normal' | 'extra' | 'full'>('normal'); - - const timelineCards: CardsData | never[] = useMemo(() => { - 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] = {}; - } - cards[dayKey][hour] = {}; - Object.values(hourlyTimeline["hours"][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[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) { - return ; - } - - return ( - <> - 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 <>; - } - - return ( -
- - {formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })} - - -
- {Object.entries(timelineHour).map(([key, timeline]) => { - return - })} -
- -
-
- ); - })} -
- ); - })} -
- - ); -} - -export default Review \ No newline at end of file diff --git a/web-new/src/types/review.ts b/web-new/src/types/history.ts similarity index 100% rename from web-new/src/types/review.ts rename to web-new/src/types/history.ts