mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 19:55:26 +03:00
Transition to history
This commit is contained in:
parent
0d707acf05
commit
d0f6316ee9
@ -7,7 +7,6 @@ import Header from "@/components/Header";
|
|||||||
import Dashboard from "@/pages/Dashboard";
|
import Dashboard from "@/pages/Dashboard";
|
||||||
import Live from "@/pages/Live";
|
import Live from "@/pages/Live";
|
||||||
import History from "@/pages/History";
|
import History from "@/pages/History";
|
||||||
import Review from "@/pages/Review";
|
|
||||||
import Export from "@/pages/Export";
|
import Export from "@/pages/Export";
|
||||||
import Storage from "@/pages/Storage";
|
import Storage from "@/pages/Storage";
|
||||||
import System from "@/pages/System";
|
import System from "@/pages/System";
|
||||||
@ -35,7 +34,6 @@ function App() {
|
|||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/live" element={<Live />} />
|
<Route path="/live" element={<Live />} />
|
||||||
<Route path="/history" element={<History />} />
|
<Route path="/history" element={<History />} />
|
||||||
<Route path="/review" element={<Review />} />
|
|
||||||
<Route path="/export" element={<Export />} />
|
<Route path="/export" element={<Export />} />
|
||||||
<Route path="/storage" element={<Storage />} />
|
<Route path="/storage" element={<Storage />} />
|
||||||
<Route path="/system" element={<System />} />
|
<Route path="/system" element={<System />} />
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { IconType } from "react-icons";
|
import { IconType } from "react-icons";
|
||||||
import { LuFileUp, LuFilm, LuLayoutDashboard, LuVideo } from "react-icons/lu";
|
import { LuFileUp, LuFilm, LuLayoutDashboard, LuVideo } from "react-icons/lu";
|
||||||
import { MdOutlinePreview } from "react-icons/md"
|
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
@ -26,12 +25,6 @@ const navbarLinks = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
icon: MdOutlinePreview,
|
|
||||||
title: "Review",
|
|
||||||
url: "/review",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
icon: LuFileUp,
|
icon: LuFileUp,
|
||||||
title: "Export",
|
title: "Export",
|
||||||
url: "/export",
|
url: "/export",
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import PreviewPlayer from "../player/PreviewPlayer";
|
import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import Heading from "../ui/heading";
|
|
||||||
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 { 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 { HiOutlineVideoCamera } from "react-icons/hi";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
|
||||||
type ReviewCardProps = {
|
type HistoryCardProps = {
|
||||||
timeline: Card,
|
timeline: Card,
|
||||||
allPreviews?: Preview[],
|
allPreviews?: Preview[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReviewCard({ allPreviews, timeline }: ReviewCardProps) {
|
export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -24,7 +23,7 @@ export default function ReviewCard({ allPreviews, timeline }: ReviewCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="my-2 mr-2 bg-secondary">
|
<Card className="my-2 mr-2 bg-secondary">
|
||||||
<PreviewPlayer
|
<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}
|
||||||
@ -9,7 +9,6 @@ type PreviewPlayerProps = {
|
|||||||
camera: string,
|
camera: string,
|
||||||
allPreviews: Preview[],
|
allPreviews: Preview[],
|
||||||
startTs: number,
|
startTs: number,
|
||||||
mode: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Preview = {
|
type Preview = {
|
||||||
@ -20,7 +19,7 @@ type Preview = {
|
|||||||
end: number,
|
end: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PreviewPlayer({ camera, allPreviews, startTs, mode }: PreviewPlayerProps) {
|
export default function PreviewThumbnailPlayer({ camera, allPreviews, startTs }: PreviewPlayerProps) {
|
||||||
const { data: config } = useSWR('config');
|
const { data: config } = useSWR('config');
|
||||||
const playerRef = useRef<Player | null>(null);
|
const playerRef = useRef<Player | null>(null);
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
@ -54,7 +53,7 @@ export default function PreviewPlayer({ camera, allPreviews, startTs, mode }: Pr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={mode == 'thumbnail' ? getThumbWidth(camera, config) : ''}
|
className={getThumbWidth(camera, config)}
|
||||||
onMouseEnter={() => onHover(true)}
|
onMouseEnter={() => onHover(true)}
|
||||||
onMouseLeave={() => onHover(false)}
|
onMouseLeave={() => onHover(false)}
|
||||||
>
|
>
|
||||||
@ -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 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() {
|
function History() {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const timezone = useMemo(() => 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 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 <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading as="h2">History</Heading>
|
<Heading as="h2">Review</Heading>
|
||||||
</>
|
<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 <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={hour}>
|
||||||
|
<Heading as="h4">
|
||||||
|
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })}
|
||||||
|
</Heading>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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<FrigateConfig>("config");
|
|
||||||
const timezone = useMemo(() => 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 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 <ActivityIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Heading as="h2">Review</Heading>
|
|
||||||
<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 <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={hour}>
|
|
||||||
<Heading as="h4">
|
|
||||||
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })}
|
|
||||||
</Heading>
|
|
||||||
<ScrollArea>
|
|
||||||
<div className="flex">
|
|
||||||
{Object.entries(timelineHour).map(([key, timeline]) => {
|
|
||||||
return <ReviewCard key={key} timeline={timeline} allPreviews={allPreviews} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ScrollBar className="m-2" orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Review
|
|
||||||
Loading…
Reference in New Issue
Block a user