mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
video overlay
This commit is contained in:
parent
28a2a3816a
commit
d3dc018260
52
web/package-lock.json
generated
52
web/package-lock.json
generated
@ -5320,11 +5320,6 @@
|
||||
"integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"desandro-matches-selector": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz",
|
||||
"integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE="
|
||||
},
|
||||
"detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
@ -6087,11 +6082,6 @@
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true
|
||||
},
|
||||
"ev-emitter": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
|
||||
"integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
|
||||
},
|
||||
"exec-sh": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
|
||||
@ -6381,14 +6371,6 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"fizzy-ui-utils": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz",
|
||||
"integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==",
|
||||
"requires": {
|
||||
"desandro-matches-selector": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"flat-cache": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||
@ -6405,19 +6387,6 @@
|
||||
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
|
||||
"dev": true
|
||||
},
|
||||
"flickity": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/flickity/-/flickity-2.2.2.tgz",
|
||||
"integrity": "sha512-yiPMuP8tw/zN7ARgeSLZNvzK11GkzI2mp/zlYBsyttguSCROAqxj6wiN2sSfPfW3xMG3hcUHxWUXNQMlk/wYcg==",
|
||||
"requires": {
|
||||
"desandro-matches-selector": "^2.0.0",
|
||||
"ev-emitter": "^1.1.1",
|
||||
"fizzy-ui-utils": "^2.0.7",
|
||||
"get-size": "^2.0.3",
|
||||
"unidragger": "^2.3.0",
|
||||
"unipointer": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
@ -6528,11 +6497,6 @@
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"get-size": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz",
|
||||
"integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q=="
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
|
||||
@ -11789,14 +11753,6 @@
|
||||
"integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
|
||||
"dev": true
|
||||
},
|
||||
"unidragger": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/unidragger/-/unidragger-2.3.1.tgz",
|
||||
"integrity": "sha512-u+IgG7AG0MXJTKcdzAIYxCm+W5FcnA9M28203Awl6jIcE3/+9OtEyUX4Wv64y7XNKEVRKPot52IV4V6x7FlF5Q==",
|
||||
"requires": {
|
||||
"unipointer": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"union-value": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
@ -11809,14 +11765,6 @@
|
||||
"set-value": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"unipointer": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unipointer/-/unipointer-2.3.0.tgz",
|
||||
"integrity": "sha512-m85sAoELCZhogI1owtJV3Dva7GxkHk2lI7A0otw3o0OwCuC/Q9gi7ehddigEYIAYbhkqNdri+dU1QQkrcBvirQ==",
|
||||
"requires": {
|
||||
"ev-emitter": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"uniq": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
|
||||
|
@ -12,7 +12,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^2.21.3",
|
||||
"flickity": "^2.2.2",
|
||||
"idb-keyval": "^5.0.2",
|
||||
"immer": "^8.0.1",
|
||||
"preact": "^10.5.9",
|
||||
|
20
web/src/components/Accordion.jsx
Normal file
20
web/src/components/Accordion.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import ArrowDropdown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
|
||||
export default function Accordion({ title, children, selected = false }) {
|
||||
const [active, setActive] = useState(selected);
|
||||
const toggle = () => setActive(!active);
|
||||
return (
|
||||
<div className="w-full border border-white border-opacity-20 rounded-md mb-4 text-xs">
|
||||
<div className="relative w-full cursor-pointer md:text-lg" onClick={toggle}>
|
||||
<div className="w-90 py-1 px-2 text-center font-bold">{title}</div>
|
||||
<div className="absolute top-0 md:-top-1 right-0 md:right-2 w-6 md:w-10 h-6 md:h-10">
|
||||
{active ? <ArrowDropup /> : <ArrowDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`bg-white bg-opacity-20 rounded-b-md p-2 ${active ? '' : 'hidden'}`}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
import Flickity from 'flickity';
|
||||
import 'flickity/css/flickity.css';
|
||||
|
||||
export default class Carousel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.carousel = null;
|
||||
this.flkty = null;
|
||||
}
|
||||
|
||||
create() {
|
||||
if (this.carousel) {
|
||||
this.flkty = new Flickity(this.carousel, this.props.options);
|
||||
|
||||
if (this.props.flickityRef) {
|
||||
this.props.flickityRef(this.flkty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.flkty) {
|
||||
this.flkty.destroy();
|
||||
this.flkty = null;
|
||||
this.carousel = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.create();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.create();
|
||||
}
|
||||
|
||||
render(props) {
|
||||
return h(
|
||||
this.props.elementType,
|
||||
{
|
||||
className: this.props.className,
|
||||
ref: (c) => {
|
||||
this.carousel = c;
|
||||
},
|
||||
},
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Carousel.defaultProps = {
|
||||
options: {},
|
||||
className: '',
|
||||
elementType: 'div',
|
||||
};
|
53
web/src/components/RecordingPlaylist.jsx
Normal file
53
web/src/components/RecordingPlaylist.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import Accordion from '../components/Accordion';
|
||||
import Link from '../components/Link';
|
||||
import Menu from '../icons/Menu';
|
||||
import MenuOpen from '../icons/MenuOpen';
|
||||
|
||||
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);
|
||||
result.push(
|
||||
<Accordion title={format(date, 'MMM d, yyyy')} selected={recording.date === selectedDate}>
|
||||
{recording.recordings.map((item) => (
|
||||
<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">
|
||||
{item.hour}:00
|
||||
</Link>
|
||||
<span className="float-right">{item.events} Events</span>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
const openClass = active ? '-left-6' : 'right-0';
|
||||
|
||||
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
|
||||
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`}
|
||||
>
|
||||
{active ? <Menu /> : <MenuOpen />}
|
||||
</div>
|
||||
<div
|
||||
className={`w-full h-full p-1 md:p-4 bg-gray-800 bg-opacity-70 border-l overflow-x-hidden overflow-y-auto${
|
||||
active ? '' : ' hidden'
|
||||
}`}
|
||||
>
|
||||
{result}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Heading({ title }) {
|
||||
return <div>{title}</div>;
|
||||
}
|
@ -31,12 +31,12 @@ export default class VideoPlayer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style } = this.props;
|
||||
const { style, children } = this.props;
|
||||
return (
|
||||
<div style={style}>
|
||||
<div data-vjs-player>
|
||||
<video playsinline ref={(node) => (this.videoNode = node)} className="video-js" />
|
||||
<div className="vjs-playlist" />
|
||||
<video ref={(node) => (this.videoNode = node)} className="video-js vjs-default-skin" controls playsinline />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -25,3 +25,11 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-playlist-modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { h } from 'preact';
|
||||
import { Link } from 'preact-router/match';
|
||||
import { closestTo, format, isEqual, parseISO } from 'date-fns';
|
||||
import { closestTo, format, parseISO } from 'date-fns';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Button from '../components/Button';
|
||||
import Calendar from '../components/Calendar';
|
||||
import Carousel from '../components/Carousel';
|
||||
import Heading from '../components/Heading';
|
||||
import RecordingPlaylist from '../components/RecordingPlaylist';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import { FetchStatus, useApiHost, useRecording } from '../api';
|
||||
|
||||
@ -24,32 +21,12 @@ export default function Recording({ camera, date, hour }) {
|
||||
);
|
||||
const selectedKey = format(selectedDate, 'yyyy-MM-dd');
|
||||
const [year, month, day] = selectedKey.split('-');
|
||||
const calendar = [];
|
||||
const buttons = [];
|
||||
const playlist = [];
|
||||
const hours = [];
|
||||
|
||||
for (const item of data) {
|
||||
const date = parseISO(item.date);
|
||||
const events = item.recordings.map((i) => i.events);
|
||||
calendar.push(
|
||||
<Link href={`/recordings/${camera}/${item.date}`}>
|
||||
<Calendar
|
||||
date={date}
|
||||
hours={events.length}
|
||||
events={events.reduce((a, b) => a + b)}
|
||||
selected={isEqual(selectedDate, date)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
||||
if (item.date === selectedKey) {
|
||||
for (const recording of item.recordings) {
|
||||
buttons.push(
|
||||
<Button href={`/recordings/${camera}/${item.date}/${recording.hour}`} type="text">
|
||||
{recording.hour}:00
|
||||
</Button>
|
||||
);
|
||||
playlist.push({
|
||||
name: `${selectedKey} ${recording.hour}:00`,
|
||||
description: `${camera} recording @ ${recording.hour}:00.`,
|
||||
@ -76,24 +53,11 @@ export default function Recording({ camera, date, hour }) {
|
||||
}
|
||||
}
|
||||
|
||||
const selectDate = (flkty) => {
|
||||
flkty.select(recordingDates.indexOf(selectedKey), false, true);
|
||||
};
|
||||
|
||||
const selectHour = (flkty) => {
|
||||
flkty.select(selectedHour, false, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Heading>{camera} Recordings</Heading>
|
||||
|
||||
<Carousel flickityRef={selectDate} options={{ pageDots: false }}>
|
||||
{calendar}
|
||||
</Carousel>
|
||||
|
||||
<VideoPlayer
|
||||
date={selectedKey}
|
||||
onReady={(player) => {
|
||||
if (player.playlist) {
|
||||
player.playlist(playlist);
|
||||
@ -104,11 +68,9 @@ export default function Recording({ camera, date, hour }) {
|
||||
this.player = player;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Carousel flickityRef={selectHour} options={{ pageDots: false }}>
|
||||
{buttons}
|
||||
</Carousel>
|
||||
>
|
||||
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} />
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user