forked from Github/frigate
revamp recordings
This commit is contained in:
@@ -1,59 +1,39 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useState, useMemo } from 'preact/hooks';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
getUnixTime,
|
||||
fromUnixTime,
|
||||
format,
|
||||
parseISO,
|
||||
startOfHour,
|
||||
differenceInMinutes,
|
||||
differenceInHours
|
||||
intervalToDuration,
|
||||
formatDuration,
|
||||
endOfDay,
|
||||
startOfDay,
|
||||
isSameDay,
|
||||
} from 'date-fns';
|
||||
import ArrowDropdown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
import Link from '../components/Link';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Menu from '../icons/Menu';
|
||||
import MenuOpen from '../icons/MenuOpen';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
|
||||
const [active, setActive] = useState(true);
|
||||
const toggle = () => setActive(!active);
|
||||
|
||||
const result = [];
|
||||
for (const recording of recordings.slice().reverse()) {
|
||||
const date = parseISO(recording.date);
|
||||
for (const recording of recordings) {
|
||||
const date = parseISO(recording.day);
|
||||
result.push(
|
||||
<ExpandableList
|
||||
title={format(date, 'MMM d, yyyy')}
|
||||
events={recording.events}
|
||||
selected={recording.date === selectedDate}
|
||||
selected={isSameDay(date, selectedDate)}
|
||||
>
|
||||
{recording.recordings
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((item, i) => (
|
||||
<div key={i} className="mb-2 w-full">
|
||||
<div
|
||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text">
|
||||
{item.hour}:00
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 text-right">{item.events.length} Events</div>
|
||||
</div>
|
||||
{item.events
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((event) => (
|
||||
<EventCard key={event.id} camera={camera} event={event} delay={item.delay} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<DayOfEvents camera={camera} day={recording.day} hours={recording.hours} />
|
||||
</ExpandableList>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +59,71 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
||||
);
|
||||
}
|
||||
|
||||
export function DayOfEvents({ camera, day, hours }) {
|
||||
const date = parseISO(day);
|
||||
const { data: events } = useSWR([
|
||||
`events`,
|
||||
{
|
||||
before: getUnixTime(endOfDay(date)),
|
||||
after: getUnixTime(startOfDay(date)),
|
||||
camera,
|
||||
has_clip: '1',
|
||||
include_thumbnails: 0,
|
||||
limit: 5000,
|
||||
},
|
||||
]);
|
||||
|
||||
// maps all the events under the keys for the hour by hour recordings view
|
||||
const eventMap = useMemo(() => {
|
||||
const eventMap = {};
|
||||
for (const hour of hours) {
|
||||
eventMap[`${day}-${hour.hour}`] = [];
|
||||
}
|
||||
|
||||
if (!events) {
|
||||
return eventMap;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const key = format(fromUnixTime(event.start_time), 'yyyy-MM-dd-HH');
|
||||
// if the hour of recordings is missing for the event start time, skip it
|
||||
if (key in eventMap) {
|
||||
eventMap[key].push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return eventMap;
|
||||
}, [events, day, hours]);
|
||||
|
||||
if (!events) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hours.map((hour, i) => (
|
||||
<div key={i} className="mb-2 w-full">
|
||||
<div
|
||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Link href={`/recording/${camera}/${day}/${hour.hour}`} type="text">
|
||||
{hour.hour}:00
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 text-right">{hour.events} Events</div>
|
||||
</div>
|
||||
{eventMap[`${day}-${hour.hour}`].map((event) => (
|
||||
<EventCard key={event.id} camera={camera} event={event} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandableList({ title, events = 0, children, selected = false }) {
|
||||
const [active, setActive] = useState(selected);
|
||||
const toggle = () => setActive(!active);
|
||||
@@ -89,35 +134,26 @@ export function ExpandableList({ title, events = 0, children, selected = false }
|
||||
<div className="flex-1 text-right mr-4">{events} Events</div>
|
||||
<div className="w-6 md:w-10 h-6 md:h-10">{active ? <ArrowDropup /> : <ArrowDropdown />}</div>
|
||||
</div>
|
||||
<div className={`bg-gray-800 bg-opacity-50 ${active ? '' : 'hidden'}`}>{children}</div>
|
||||
{/* Only render the child when expanded to lazy load events for the day */}
|
||||
{active && <div className={`bg-gray-800 bg-opacity-50`}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventCard({ camera, event, delay }) {
|
||||
export function EventCard({ camera, event }) {
|
||||
const apiHost = useApiHost();
|
||||
const start = fromUnixTime(event.start_time);
|
||||
const end = fromUnixTime(event.end_time);
|
||||
let duration = 'In Progress';
|
||||
if (event.end_time) {
|
||||
const end = fromUnixTime(event.end_time);
|
||||
const hours = differenceInHours(end, start);
|
||||
const minutes = differenceInMinutes(end, start) - hours * 60;
|
||||
const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60;
|
||||
duration = '';
|
||||
if (hours) duration += `${hours}h `;
|
||||
if (minutes) duration += `${minutes}m `;
|
||||
duration += `${seconds}s`;
|
||||
duration = formatDuration(intervalToDuration({ start, end }));
|
||||
}
|
||||
const position = differenceInSeconds(start, startOfHour(start));
|
||||
const offset = Object.entries(delay)
|
||||
.map(([p, d]) => (position > p ? d : 0))
|
||||
.reduce((p, c) => p + c, 0);
|
||||
const seconds = Math.max(position - offset - 10, 0);
|
||||
|
||||
return (
|
||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
|
||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
|
||||
<div className="flex flex-row mb-2">
|
||||
<div className="w-28 mr-4">
|
||||
<img className="antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||
<img className="antialiased" loading="lazy" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||
</div>
|
||||
<div className="flex flex-row w-full border-b">
|
||||
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
||||
|
||||
Reference in New Issue
Block a user