mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 01:05:20 +03:00
useOuterClick hook
This commit is contained in:
parent
d92c945c10
commit
abbbce0481
@ -14,9 +14,9 @@ export function Thead({ children, className, ...attrs }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tbody({ children, className, ...attrs }) {
|
export function Tbody({ children, className, reference, ...attrs }) {
|
||||||
return (
|
return (
|
||||||
<tbody className={className} {...attrs}>
|
<tbody ref={reference} className={className} {...attrs}>
|
||||||
{children}
|
{children}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
@ -30,9 +30,10 @@ export function Tfoot({ children, className = '', ...attrs }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tr({ children, className = '', ...attrs }) {
|
export function Tr({ children, className = '', reference, ...attrs }) {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
ref={reference}
|
||||||
className={`border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 ${className}`}
|
className={`border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 ${className}`}
|
||||||
{...attrs}
|
{...attrs}
|
||||||
>
|
>
|
||||||
@ -49,9 +50,9 @@ export function Th({ children, className = '', colspan, ...attrs }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Td({ children, className = '', colspan, ...attrs }) {
|
export function Td({ children, className = '', reference, colspan, ...attrs }) {
|
||||||
return (
|
return (
|
||||||
<td className={`p-2 px-1 lg:p-4 ${className}`} colSpan={colspan} {...attrs}>
|
<td ref={reference} className={`p-2 px-1 lg:p-4 ${className}`} colSpan={colspan} {...attrs}>
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,5 +36,5 @@ Maintain aspect ratio and scale down the video container
|
|||||||
Could not find a proper tailwind css.
|
Could not find a proper tailwind css.
|
||||||
*/
|
*/
|
||||||
.outer-max-width {
|
.outer-max-width {
|
||||||
max-width: 60%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { h, Fragment } from 'preact';
|
import { h, Fragment } from 'preact';
|
||||||
import { useCallback, useState, useEffect, useRef } from 'preact/hooks';
|
import { useCallback, useState, useEffect, useRef } from 'preact/hooks';
|
||||||
|
import Link from '../components/Link';
|
||||||
import ActivityIndicator from '../components/ActivityIndicator';
|
import ActivityIndicator from '../components/ActivityIndicator';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
|
import ArrowDown from '../icons/ArrowDropdown';
|
||||||
import Clip from '../icons/Clip';
|
import Clip from '../icons/Clip';
|
||||||
import Close from '../icons/Close';
|
import Close from '../icons/Close';
|
||||||
import Delete from '../icons/Delete';
|
import Delete from '../icons/Delete';
|
||||||
@ -9,16 +11,17 @@ import Snapshot from '../icons/Snapshot';
|
|||||||
import Dialog from '../components/Dialog';
|
import Dialog from '../components/Dialog';
|
||||||
import Heading from '../components/Heading';
|
import Heading from '../components/Heading';
|
||||||
import VideoPlayer from '../components/VideoPlayer';
|
import VideoPlayer from '../components/VideoPlayer';
|
||||||
|
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
||||||
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
||||||
|
|
||||||
export default function Event({ eventId, close, scrollRef }) {
|
export default function Event({ eventId, close, scrollRef }) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const { data, status } = useEvent(eventId);
|
const { data, status } = useEvent(eventId);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
const [shouldScroll, setShouldScroll] = useState(true);
|
const [shouldScroll, setShouldScroll] = useState(true);
|
||||||
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
||||||
const setDeleteEvent = useDelete();
|
const setDeleteEvent = useDelete();
|
||||||
const eventRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Scroll event into view when component has been mounted.
|
// Scroll event into view when component has been mounted.
|
||||||
@ -54,9 +57,10 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
if (status !== FetchStatus.LOADED) {
|
if (status !== FetchStatus.LOADED) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
const startime = new Date(data.start_time * 1000);
|
||||||
|
const endtime = new Date(data.end_time * 1000);
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4" ref={eventRef}>
|
<div className="space-y-4">
|
||||||
<div className="flex md:flex-row justify-between flex-wrap flex-col">
|
<div className="flex md:flex-row justify-between flex-wrap flex-col">
|
||||||
<div class="space-x-4">
|
<div class="space-x-4">
|
||||||
<Button color="blue" href={`${apiHost}/api/events/${eventId}/clip.mp4?download=true`} download>
|
<Button color="blue" href={`${apiHost}/api/events/${eventId}/clip.mp4?download=true`} download>
|
||||||
@ -65,6 +69,10 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
<Button color="blue" href={`${apiHost}/api/events/${eventId}/snapshot.jpg?download=true`} download>
|
<Button color="blue" href={`${apiHost}/api/events/${eventId}/snapshot.jpg?download=true`} download>
|
||||||
<Snapshot className="w-6" /> Download Snapshot
|
<Snapshot className="w-6" /> Download Snapshot
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button className="self-start" onClick={() => setShowDetails(!showDetails)}>
|
||||||
|
<ArrowDown className="w-6" />
|
||||||
|
{`${showDetails ? 'Hide event Details' : 'View event Details'}`}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-4">
|
<div class="space-x-4">
|
||||||
<Button className="self-start" color="red" onClick={handleClickDelete}>
|
<Button className="self-start" color="red" onClick={handleClickDelete}>
|
||||||
@ -92,13 +100,46 @@ export default function Event({ eventId, close, scrollRef }) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
{showDetails ? (
|
||||||
|
<Table class="w-full">
|
||||||
|
<Thead>
|
||||||
|
<Th>Key</Th>
|
||||||
|
<Th>Value</Th>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td>Camera</Td>
|
||||||
|
<Td>
|
||||||
|
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr index={1}>
|
||||||
|
<Td>Timeframe</Td>
|
||||||
|
<Td>
|
||||||
|
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Score</Td>
|
||||||
|
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr index={1}>
|
||||||
|
<Td>Zones</Td>
|
||||||
|
<Td>{data.zones.join(', ')}</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="outer-max-width xs:m-auto">
|
<div className="outer-max-width xs:m-auto">
|
||||||
<div className="w-full pt-5 relative pb-20">
|
<div className="pt-5 relative pb-20">
|
||||||
{data.has_clip ? (
|
{data.has_clip ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Heading size="lg">Clip</Heading>
|
<Heading size="lg">Clip</Heading>
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
options={{
|
options={{
|
||||||
|
// preload: 'none',
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
src: `${apiHost}/vod/event/${eventId}/index.m3u8`,
|
src: `${apiHost}/vod/event/${eventId}/index.m3u8`,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { h, Fragment } from 'preact';
|
import { h, Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useCallback, useState, useMemo } from 'preact/hooks';
|
import { useCallback, useState, useMemo, useEffect } from 'preact/hooks';
|
||||||
import { Tr, Td } from '../../../components/Table';
|
import { Tr, Td, Tbody } from '../../../components/Table';
|
||||||
import Filterable from './filterable';
|
import Filterable from './filterable';
|
||||||
import Event from '../../Event';
|
import Event from '../../Event';
|
||||||
import { useSearchString } from '../hooks/useSearchString';
|
import { useSearchString } from '../hooks/useSearchString';
|
||||||
|
import { useOuterClick } from '../hooks/useClickOutside';
|
||||||
|
|
||||||
const EventsRow = memo(
|
const EventsRow = memo(
|
||||||
({
|
({
|
||||||
@ -26,6 +27,10 @@ const EventsRow = memo(
|
|||||||
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
||||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||||
|
|
||||||
|
const innerRef = useOuterClick(() => {
|
||||||
|
setViewEvent(null);
|
||||||
|
});
|
||||||
|
|
||||||
const viewEventHandler = useCallback(
|
const viewEventHandler = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
//Toggle event view
|
//Toggle event view
|
||||||
@ -39,8 +44,9 @@ const EventsRow = memo(
|
|||||||
const start = new Date(parseInt(startTime * 1000, 10));
|
const start = new Date(parseInt(startTime * 1000, 10));
|
||||||
const end = new Date(parseInt(endTime * 1000, 10));
|
const end = new Date(parseInt(endTime * 1000, 10));
|
||||||
console.log('tablerow has been rendered');
|
console.log('tablerow has been rendered');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={id}>
|
<Tbody reference={innerRef} key={id}>
|
||||||
<Tr data-testid={`event-${id}`} className={`${viewEvent === id ? 'border-none' : ''}`}>
|
<Tr data-testid={`event-${id}`} className={`${viewEvent === id ? 'border-none' : ''}`}>
|
||||||
<Td className="w-40">
|
<Td className="w-40">
|
||||||
<a
|
<a
|
||||||
@ -50,7 +56,6 @@ const EventsRow = memo(
|
|||||||
// data-reached-end={reachedEnd}
|
// data-reached-end={reachedEnd}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref={(el) => (scrollToRef[id] = el)}
|
|
||||||
width="150"
|
width="150"
|
||||||
height="150"
|
height="150"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@ -102,12 +107,12 @@ const EventsRow = memo(
|
|||||||
</Tr>
|
</Tr>
|
||||||
{viewEvent === id ? (
|
{viewEvent === id ? (
|
||||||
<Tr className="border-b-1">
|
<Tr className="border-b-1">
|
||||||
<Td colSpan="8">
|
<Td colSpan="8" reference={(el) => (scrollToRef[id] = el)}>
|
||||||
<Event eventId={id} close={() => setViewEvent(null)} scrollRef={scrollToRef} />
|
<Event eventId={id} close={() => setViewEvent(null)} scrollRef={scrollToRef} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
) : null}
|
) : null}
|
||||||
</Fragment>
|
</Tbody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
21
web/src/routes/Events/hooks/useClickOutside.jsx
Normal file
21
web/src/routes/Events/hooks/useClickOutside.jsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export const useOuterClick = (callback) => {
|
||||||
|
const callbackRef = useRef(); // initialize mutable ref, which stores callback
|
||||||
|
const innerRef = useRef(); // returned to client, who marks "border" element
|
||||||
|
|
||||||
|
// update cb on each render, so second useEffect has access to current value
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleClick);
|
||||||
|
return () => document.removeEventListener('click', handleClick);
|
||||||
|
function handleClick(e) {
|
||||||
|
if (innerRef.current && callbackRef.current && !innerRef.current.contains(e.target)) callbackRef.current(e);
|
||||||
|
}
|
||||||
|
}, []); // no dependencies -> stable click listener
|
||||||
|
|
||||||
|
return innerRef; // convenience for client (doesn't need to init ref himself)
|
||||||
|
};
|
||||||
@ -4,7 +4,7 @@ import Heading from '../../components/Heading';
|
|||||||
import { TableHead, Filters, TableRow } from './components';
|
import { TableHead, Filters, TableRow } from './components';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { FetchStatus, useApiHost, useEvents } from '../../api';
|
import { FetchStatus, useApiHost, useEvents } from '../../api';
|
||||||
import { Table, Tbody, Tfoot, Tr, Td } from '../../components/Table';
|
import { Table, Tfoot, Tr, Td } from '../../components/Table';
|
||||||
import { useCallback, useEffect, useMemo, useReducer } from 'preact/hooks';
|
import { useCallback, useEffect, useMemo, useReducer } from 'preact/hooks';
|
||||||
import { reducer, initialState } from './reducer';
|
import { reducer, initialState } from './reducer';
|
||||||
import { useSearchString } from './hooks/useSearchString';
|
import { useSearchString } from './hooks/useSearchString';
|
||||||
@ -89,12 +89,12 @@ export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
|||||||
<div className="min-w-0 overflow-auto">
|
<div className="min-w-0 overflow-auto">
|
||||||
<Table className="min-w-full table-fixed">
|
<Table className="min-w-full table-fixed">
|
||||||
<TableHead />
|
<TableHead />
|
||||||
<Tbody>
|
|
||||||
{events.map((props, idx) => {
|
{events.map((props, idx) => {
|
||||||
const lastRowRef = idx === events.length - 1 ? lastCellRef : undefined;
|
const lastRowRef = idx === events.length - 1 ? lastCellRef : undefined;
|
||||||
return <RenderTableRow {...props} lastRowRef={lastRowRef} idx={idx} />;
|
return <RenderTableRow {...props} lastRowRef={lastRowRef} idx={idx} />;
|
||||||
})}
|
})}
|
||||||
</Tbody>
|
|
||||||
<Tfoot>
|
<Tfoot>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td className="text-center p-4" colSpan="8">
|
<Td className="text-center p-4" colSpan="8">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user