mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
add event card to overlay
This commit is contained in:
parent
d3dc018260
commit
ca20c735f7
@ -468,26 +468,17 @@ def recordings(camera_name):
|
|||||||
date = f"{search.group(1)}-{search.group(2)}"
|
date = f"{search.group(1)}-{search.group(2)}"
|
||||||
if date not in dates:
|
if date not in dates:
|
||||||
dates[date] = OrderedDict()
|
dates[date] = OrderedDict()
|
||||||
dates[date][search.group(3)] = 0
|
dates[date][search.group(3)] = []
|
||||||
|
|
||||||
events = (
|
events = Event.select().where(Event.camera == camera_name)
|
||||||
Event.select(
|
|
||||||
fn.DATE(Event.start_time, "unixepoch", "localtime"),
|
|
||||||
fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
|
|
||||||
fn.COUNT(Event.id),
|
|
||||||
)
|
|
||||||
.where(Event.camera == camera_name)
|
|
||||||
.group_by(
|
|
||||||
fn.DATE(Event.start_time, "unixepoch", "localtime"),
|
|
||||||
fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
|
|
||||||
)
|
|
||||||
.tuples()
|
|
||||||
)
|
|
||||||
|
|
||||||
for date, hour, count in events:
|
e: Event
|
||||||
|
for e in events:
|
||||||
|
date = datetime.fromtimestamp(e.start_time)
|
||||||
key = date.strftime("%Y-%m-%d")
|
key = date.strftime("%Y-%m-%d")
|
||||||
|
hour = date.strftime("%H")
|
||||||
if key in dates and hour in dates[key]:
|
if key in dates and hour in dates[key]:
|
||||||
dates[key][hour] = count
|
dates[key][hour].append(model_to_dict(e, exclude=[Event.thumbnail]))
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
[
|
[
|
||||||
|
@ -29,7 +29,7 @@ export default function App() {
|
|||||||
<AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
|
<AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
|
||||||
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
|
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
|
||||||
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
||||||
<AsyncRoute path="/recordings/:camera/:date?/:hour?" getComponent={Routes.getRecording} />
|
<AsyncRoute path="/recordings/:camera/:date?/:hour?/:seconds?" getComponent={Routes.getRecording} />
|
||||||
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
|
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
|
||||||
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
|
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
|
||||||
<Cameras default path="/" />
|
<Cameras default path="/" />
|
||||||
|
38
web/src/components/EventCard.jsx
Normal file
38
web/src/components/EventCard.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { differenceInSeconds, fromUnixTime, format, startOfHour } from 'date-fns';
|
||||||
|
import Link from '../components/Link';
|
||||||
|
import { useApiHost } from '../api';
|
||||||
|
|
||||||
|
export default function EventCard({ camera, event }) {
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
const start = fromUnixTime(event.start_time);
|
||||||
|
const end = fromUnixTime(event.end_time);
|
||||||
|
const seconds = Math.max(differenceInSeconds(start, startOfHour(start)) - 10, 0);
|
||||||
|
return (
|
||||||
|
<Link className="" href={`/recordings/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
|
||||||
|
<div className="rounded-lg shadow-lg bg-gray-600 w-full flex flex-row flex-wrap p-3 antialiased mb-2">
|
||||||
|
<div className="w-1/2 md:w-1/3">
|
||||||
|
<img className="rounded-lg shadow-lg antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 md:w-2/3 px-3 flex flex-row flex-wrap">
|
||||||
|
<div className="w-full text-right text-gray-700 font-semibold relative pt-0">
|
||||||
|
<div className="text-2xl text-white leading-tight capitalize">{event.label}</div>
|
||||||
|
<div className="text-lg text-white leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
|
||||||
|
<div className="text-xs md:text-normal text-gray-300 hover:text-gray-400 cursor-pointer">
|
||||||
|
<span className="border-b border-dashed border-gray-500 pb-1">
|
||||||
|
{format(start, 'HH:mm:ss')} - {format(end, 'HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-full text-right">
|
||||||
|
<div className="text-sm text-gray-300 hover:text-gray-400 cursor-pointer md:absolute pt-3 md:pt-0 bottom-0 right-0">
|
||||||
|
{event.zones.map((zone) => (
|
||||||
|
<div>{zone}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { h } from 'preact';
|
|||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import Accordion from '../components/Accordion';
|
import Accordion from '../components/Accordion';
|
||||||
|
import EventCard from '../components/EventCard';
|
||||||
import Link from '../components/Link';
|
import Link from '../components/Link';
|
||||||
import Menu from '../icons/Menu';
|
import Menu from '../icons/Menu';
|
||||||
import MenuOpen from '../icons/MenuOpen';
|
import MenuOpen from '../icons/MenuOpen';
|
||||||
@ -16,11 +17,16 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
|||||||
result.push(
|
result.push(
|
||||||
<Accordion title={format(date, 'MMM d, yyyy')} selected={recording.date === selectedDate}>
|
<Accordion title={format(date, 'MMM d, yyyy')} selected={recording.date === selectedDate}>
|
||||||
{recording.recordings.map((item) => (
|
{recording.recordings.map((item) => (
|
||||||
|
<div className="mb-2">
|
||||||
<div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
|
<div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
|
||||||
<Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
|
<Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
|
||||||
{item.hour}:00
|
{item.hour}:00
|
||||||
</Link>
|
</Link>
|
||||||
<span className="float-right">{item.events} Events</span>
|
<span className="float-right">{item.events.length} Events</span>
|
||||||
|
</div>
|
||||||
|
{item.events.map((event) => (
|
||||||
|
<EventCard camera={camera} event={event} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@ -30,7 +36,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
|||||||
const openClass = active ? '-left-6' : 'right-0';
|
const openClass = active ? '-left-6' : 'right-0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex absolute inset-y-0 right-0 w-1/2 md:w-1/3 max-w-xl min-w-lg text-base text-white font-sans">
|
<div className="flex absolute inset-y-0 right-0 w-9/12 md:w-1/3 max-w-xl min-w-lg text-base text-white font-sans">
|
||||||
<div
|
<div
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`}
|
className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`}
|
||||||
|
@ -6,7 +6,7 @@ import RecordingPlaylist from '../components/RecordingPlaylist';
|
|||||||
import VideoPlayer from '../components/VideoPlayer';
|
import VideoPlayer from '../components/VideoPlayer';
|
||||||
import { FetchStatus, useApiHost, useRecording } from '../api';
|
import { FetchStatus, useApiHost, useRecording } from '../api';
|
||||||
|
|
||||||
export default function Recording({ camera, date, hour }) {
|
export default function Recording({ camera, date, hour, seconds }) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const { data, status } = useRecording(camera);
|
const { data, status } = useRecording(camera);
|
||||||
|
|
||||||
@ -50,6 +50,9 @@ export default function Recording({ camera, date, hour }) {
|
|||||||
this.player.playlist.autoadvance(0);
|
this.player.playlist.autoadvance(0);
|
||||||
if (selectedHour !== -1) {
|
if (selectedHour !== -1) {
|
||||||
this.player.playlist.currentItem(selectedHour);
|
this.player.playlist.currentItem(selectedHour);
|
||||||
|
if (seconds !== undefined) {
|
||||||
|
this.player.currentTime(seconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,9 @@ export default function Recording({ camera, date, hour }) {
|
|||||||
player.playlist.autoadvance(0);
|
player.playlist.autoadvance(0);
|
||||||
if (selectedHour !== -1) {
|
if (selectedHour !== -1) {
|
||||||
player.playlist.currentItem(selectedHour);
|
player.playlist.currentItem(selectedHour);
|
||||||
|
if (seconds !== undefined) {
|
||||||
|
player.currentTime(seconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user