refactoring
add translates finish recordings
This commit is contained in:
parent
a971ea55a4
commit
7304fe231e
@ -1,6 +1,6 @@
|
||||
import { Button, Flex, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { redirect, useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button, Flex, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { redirect, useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
|
||||
@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { Context } from '..';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { Button, Flex } from '@mantine/core';
|
||||
@ -71,7 +71,7 @@ const FrigateHostsPage = observer(() => {
|
||||
}
|
||||
|
||||
if (hostsPending) return <CenterLoader />
|
||||
if (hostsError) return <RetryError />
|
||||
if (hostsError) return <RetryErrorPage />
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
|
||||
@ -9,7 +9,7 @@ import { configureMonacoYaml } from "monaco-yaml";
|
||||
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
|
||||
import * as monaco from "monaco-editor";
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
|
||||
|
||||
const HostConfigPage = () => {
|
||||
@ -74,7 +74,7 @@ const HostConfigPage = () => {
|
||||
|
||||
if (configPending) return <CenterLoader />
|
||||
|
||||
if (configError) return <RetryError onRetry={refetch} />
|
||||
if (configError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
return (
|
||||
<Flex direction='column' h='100%' w='100%' justify='stretch'>
|
||||
|
||||
@ -5,8 +5,8 @@ import { useParams } from 'react-router-dom';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import Player from '../shared/components/frigate/Player';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import Player from '../widgets/Player';
|
||||
import { Flex } from '@mantine/core';
|
||||
|
||||
const LiveCameraPage = observer(() => {
|
||||
@ -28,7 +28,7 @@ const LiveCameraPage = observer(() => {
|
||||
|
||||
if (isPending) return <CenterLoader />
|
||||
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' justify='center'>
|
||||
|
||||
@ -8,11 +8,12 @@ import { observer } from 'mobx-react-lite'
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import RetryError from './RetryError';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import CameraCard from '../shared/components/CameraCard';
|
||||
|
||||
const MainBody = observer(() => {
|
||||
const MainPage = () => {
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = false
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
@ -32,11 +33,11 @@ const MainBody = observer(() => {
|
||||
|
||||
if (isPending) return <CenterLoader />
|
||||
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
const cards = () => {
|
||||
// return cameras.filter(cam => cam.frigateHost?.host.includes('5001')).slice(0,1).map(camera => (
|
||||
return cameras.map(camera => (
|
||||
return cameras.filter(cam => cam.frigateHost?.host.includes('5000')).slice(0,25).map(camera => (
|
||||
// return cameras.map(camera => (
|
||||
<CameraCard
|
||||
key={camera.id}
|
||||
camera={camera}
|
||||
@ -70,6 +71,6 @@ const MainBody = observer(() => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
export default MainBody;
|
||||
export default observer(MainPage);
|
||||
@ -9,12 +9,18 @@ import SelectedCameraList from '../widgets/SelectedCameraList';
|
||||
import SelectedHostList from '../widgets/SelectedHostList';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
|
||||
import SelecteDayList from '../widgets/SelecteDayList';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
|
||||
|
||||
const recordingsQuery = {
|
||||
export const recordingsPageQuery = {
|
||||
hostId: 'hostId',
|
||||
cameraId: 'cameraId',
|
||||
date: 'date',
|
||||
startDay: 'startDay',
|
||||
endDay: 'endDay',
|
||||
hour: 'hour',
|
||||
}
|
||||
|
||||
@ -24,13 +30,16 @@ const RecordingsPage = observer(() => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
const paramHostId = queryParams.get(recordingsQuery.hostId)
|
||||
const paramCameraId = queryParams.get(recordingsQuery.cameraId);
|
||||
const paramDate = queryParams.get(recordingsQuery.date);
|
||||
const paramTime = queryParams.get(recordingsQuery.hour);
|
||||
const paramHostId = queryParams.get(recordingsPageQuery.hostId)
|
||||
const paramCameraId = queryParams.get(recordingsPageQuery.cameraId);
|
||||
const paramStartDay = queryParams.get(recordingsPageQuery.startDay);
|
||||
const paramEndDay = queryParams.get(recordingsPageQuery.endDay);
|
||||
const paramTime = queryParams.get(recordingsPageQuery.hour);
|
||||
|
||||
const [hostId, setHostId] = useState<string>('')
|
||||
const [cameraId, setCameraId] = useState<string>('')
|
||||
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
|
||||
const [firstRender, setFirstRender] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = true
|
||||
@ -39,15 +48,21 @@ const RecordingsPage = observer(() => {
|
||||
)
|
||||
if (paramHostId) recStore.hostIdParam = paramHostId
|
||||
if (paramCameraId) recStore.cameraIdParam = paramCameraId
|
||||
if (paramStartDay && paramEndDay) {
|
||||
const parsedStartDay = parseQueryDateToDate(paramStartDay)
|
||||
const parsedEndDay = parseQueryDateToDate(paramEndDay)
|
||||
recStore.selectedRange = [parsedStartDay, parsedEndDay]
|
||||
}
|
||||
setFirstRender(true)
|
||||
return () => sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setHostId(recStore.selectedHost?.id || '')
|
||||
if (recStore.selectedHost) {
|
||||
queryParams.set(recordingsQuery.hostId, recStore.selectedHost.id)
|
||||
queryParams.set(recordingsPageQuery.hostId, recStore.selectedHost.id)
|
||||
} else {
|
||||
queryParams.delete(recordingsQuery.hostId)
|
||||
queryParams.delete(recordingsPageQuery.hostId)
|
||||
}
|
||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||
}, [recStore.selectedHost])
|
||||
@ -55,26 +70,61 @@ const RecordingsPage = observer(() => {
|
||||
useEffect(() => {
|
||||
setCameraId(recStore.selectedCamera?.id || '')
|
||||
if (recStore.selectedCamera) {
|
||||
queryParams.set(recordingsQuery.cameraId, recStore.selectedCamera?.id)
|
||||
queryParams.set(recordingsPageQuery.cameraId, recStore.selectedCamera?.id)
|
||||
} else {
|
||||
console.log('delete recordingsQuery.cameraId')
|
||||
queryParams.delete(recordingsQuery.cameraId)
|
||||
queryParams.delete(recordingsPageQuery.cameraId)
|
||||
}
|
||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||
}, [recStore.selectedCamera])
|
||||
|
||||
if (cameraId) {
|
||||
return <SelectedCameraList cameraId={cameraId} />
|
||||
useEffect(() => {
|
||||
setPeriod(recStore.selectedRange)
|
||||
const [startDay, endDay] = recStore.selectedRange
|
||||
if (startDay && endDay) {
|
||||
const startQuery = dateToQueryString(startDay)
|
||||
const endQuery = dateToQueryString(endDay)
|
||||
queryParams.set(recordingsPageQuery.startDay, startQuery)
|
||||
queryParams.set(recordingsPageQuery.endDay, endQuery)
|
||||
} else {
|
||||
queryParams.delete(recordingsPageQuery.startDay)
|
||||
queryParams.delete(recordingsPageQuery.endDay)
|
||||
}
|
||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||
}, [recStore.selectedRange])
|
||||
|
||||
console.log('RecordingsPage rendered')
|
||||
|
||||
if (!firstRender) return <CenterLoader />
|
||||
|
||||
const [startDay, endDay] = period
|
||||
if (startDay && endDay) {
|
||||
if (startDay.getDate() === endDay.getDate()) { // if select only one day
|
||||
return <SelecteDayList day={startDay} />
|
||||
}
|
||||
}
|
||||
|
||||
if (hostId) {
|
||||
if (cameraId && paramCameraId) {
|
||||
// console.log('cameraId', cameraId)
|
||||
// console.log('paramCameraId', paramCameraId)
|
||||
if ((startDay && endDay) || (!startDay && !endDay)) {
|
||||
return <SelectedCameraList />
|
||||
// return <SelectedCameraList cameraId={cameraId} />
|
||||
}
|
||||
}
|
||||
|
||||
if (hostId && paramHostId && !cameraId) {
|
||||
return <SelectedHostList hostId={hostId} />
|
||||
}
|
||||
|
||||
console.log('RecordingsPage rendered')
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
|
||||
{!hostId ?
|
||||
<Text size='xl'>Please select host</Text>
|
||||
: <></>}
|
||||
{hostId && !(startDay && endDay) ?
|
||||
<Text size='xl'>Please select date</Text>
|
||||
: <></>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
|
||||
@ -6,11 +6,11 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||
import { Context } from '..';
|
||||
|
||||
interface RetryErrorProps {
|
||||
interface RetryErrorPageProps {
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
const RetryError = ({ onRetry }: RetryErrorProps) => {
|
||||
const RetryErrorPage = ({ onRetry }: RetryErrorPageProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
@ -48,4 +48,4 @@ const RetryError = ({ onRetry }: RetryErrorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RetryError;
|
||||
export default RetryErrorPage;
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { Button, Flex, Space } from '@mantine/core';
|
||||
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
@ -83,7 +83,7 @@ const SettingsPage = () => {
|
||||
|
||||
if (configPending) return <CenterLoader />
|
||||
|
||||
if (configError) return <RetryError onRetry={refetch} />
|
||||
if (configError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
return (
|
||||
<Flex h='100%'>
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import React, { Fragment, useContext, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import JSMpegPlayer from '../shared/components/frigate/JSMpegPlayer';
|
||||
import { frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { Flex } from '@mantine/core';
|
||||
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
|
||||
|
||||
const Test = observer(() => {
|
||||
// const test = {
|
||||
// camera: 'Buhgalteria',
|
||||
// host: 'localhost:5000',
|
||||
// width: 800,
|
||||
// height: 600,
|
||||
// url : function() { return frigateApi.cameraWsURL(this.host, this.camera)},
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <Flex w='100%' h='100%'>
|
||||
// <JSMpegPlayer wsUrl={test.url()} camera={test.camera} width={test.width} height={test.height} />
|
||||
// </Flex>
|
||||
// );
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%'>
|
||||
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
|
||||
export default Test;
|
||||
18
src/pages/TestItem.tsx
Normal file
18
src/pages/TestItem.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Paper } from '@mantine/core';
|
||||
import { useIntersection } from '@mantine/hooks';
|
||||
import React from 'react';
|
||||
|
||||
const TestItem = () => {
|
||||
|
||||
const { ref, entry } = useIntersection({threshold: 0.1,})
|
||||
|
||||
return (<Paper
|
||||
ref={ref}
|
||||
m='0.2rem' w='10rem' h='10rem'
|
||||
sx={(theme) => ({
|
||||
backgroundColor: entry?.isIntersecting ? theme.colors.green[9] : theme.colors.red[9],
|
||||
})}
|
||||
/>)
|
||||
};
|
||||
|
||||
export default TestItem;
|
||||
59
src/pages/TestPage.tsx
Normal file
59
src/pages/TestPage.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { Fragment, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button, Flex, Grid, Group, Indicator, Paper, Skeleton } from '@mantine/core';
|
||||
import RetryError from '../shared/components/RetryError';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import HeadSearch from '../shared/components/HeadSearch';
|
||||
import ViewSelector from '../shared/components/ViewSelector';
|
||||
import { useIntersection } from '@mantine/hooks';
|
||||
import TestItem from './TestItem';
|
||||
|
||||
const Test = observer(() => {
|
||||
const [value, setValue] = useState<[Date | null, Date | null]>([null, null])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('value', value)
|
||||
}, [value])
|
||||
|
||||
const handleClick = () => {
|
||||
const startOfDay = new Date();
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
setValue([startOfDay, startOfDay])
|
||||
}
|
||||
|
||||
const cards = (qty: number) => {
|
||||
let items = []
|
||||
for (let i = 0; i < qty; i++) {
|
||||
items.push(<TestItem key={i} />)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction='column' h='100%' >
|
||||
<Flex justify='space-between' align='center' w='100%'>
|
||||
<Group
|
||||
w='25%'
|
||||
>
|
||||
</Group>
|
||||
<Group
|
||||
w='50%'
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
><HeadSearch /></Group>
|
||||
<Group
|
||||
w='25%'
|
||||
position="right">
|
||||
</Group>
|
||||
</Flex>
|
||||
<Flex justify='center' h='100%' direction='column' >
|
||||
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||
{cards(60)}
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
|
||||
export default Test;
|
||||
@ -1,8 +1,8 @@
|
||||
import {JSX} from "react";
|
||||
import Test from "../pages/Test"
|
||||
import MainBody from "../pages/MainBody";
|
||||
import Test from "../pages/TestPage"
|
||||
import MainPage from "../pages/MainPage";
|
||||
import {routesPath} from "./routes.path";
|
||||
import RetryError from "../pages/RetryError";
|
||||
import RetryErrorPage from "../pages/RetryErrorPage";
|
||||
import Forbidden from "../pages/403";
|
||||
import NotFound from "../pages/404";
|
||||
import SettingsPage from "../pages/SettingsPage";
|
||||
@ -53,11 +53,11 @@ export const routes: IRoute[] = [
|
||||
},
|
||||
{
|
||||
path: routesPath.MAIN_PATH,
|
||||
component: <MainBody />,
|
||||
component: <MainPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.RETRY_ERROR_PATH,
|
||||
component: <RetryError />,
|
||||
component: <RetryErrorPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.FORBIDDEN_ERROR_PATH,
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { CameraConfig } from "../../../types/frigateConfig";
|
||||
import { AspectRatio, Flex, createStyles, Text } from "@mantine/core";
|
||||
import { CameraConfig } from "../../types/frigateConfig";
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import CenterLoader from "../CenterLoader";
|
||||
import axios from "axios";
|
||||
import { frigateApi, proxyApi } from "../../../services/frigate.proxy/frigate.api";
|
||||
import CenterLoader from "./CenterLoader";
|
||||
import { frigateApi, proxyApi } from "../../services/frigate.proxy/frigate.api";
|
||||
import { useIntersection } from "@mantine/hooks";
|
||||
import CogwheelLoader from "./loaders/CogwheelLoader";
|
||||
|
||||
interface CameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
interface AutoUpdatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
className?: string;
|
||||
cameraConfig?: CameraConfig;
|
||||
onload?: () => void;
|
||||
@ -18,26 +19,29 @@ const AutoUpdatedImage = ({
|
||||
imageUrl,
|
||||
enabled,
|
||||
...rest
|
||||
}: CameraImageProps) => {
|
||||
}: AutoUpdatedImageProps) => {
|
||||
const { ref, entry } = useIntersection({threshold: 0.1,})
|
||||
const isVisible = entry?.isIntersecting
|
||||
|
||||
const { data: imageBlob, refetch, isPending, isError } = useQuery({
|
||||
queryKey: ['image', imageUrl],
|
||||
queryFn: () => proxyApi.getImageFrigate(imageUrl),
|
||||
staleTime: 60 * 1000,
|
||||
gcTime: Infinity,
|
||||
refetchInterval: 60 * 1000,
|
||||
refetchInterval: isVisible ? 30 * 1000 : undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
console.log('imageUrl is visible')
|
||||
const intervalId = setInterval(() => {
|
||||
refetch();
|
||||
}, 60 * 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [refetch]);
|
||||
}
|
||||
}, [refetch, isVisible]);
|
||||
|
||||
|
||||
|
||||
if (isPending) return <CenterLoader />
|
||||
if (isPending) return <CogwheelLoader />
|
||||
|
||||
if (isError) return (
|
||||
<Flex direction="column" justify="center" h="100%">
|
||||
@ -51,7 +55,7 @@ const AutoUpdatedImage = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{enabled ? <img src={image} alt="Dynamic Content" {...rest}/>
|
||||
{enabled ? <img ref={ref} src={image} alt="Dynamic Content" {...rest}/>
|
||||
:
|
||||
<Flex direction="column" justify="center" h="100%">
|
||||
<Text align="center">Camera is disabled in config, no stream or snapshot available!</Text>
|
||||
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { CameraConfig } from '../../types/frigateConfig';
|
||||
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
|
||||
import AutoUpdatingCameraImage from './frigate/AutoUpdatingCameraImage';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../../router/routes.path';
|
||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
|
||||
import { frigateApi, mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api';
|
||||
import AutoUpdatedImage from './frigate/CameraImage';
|
||||
import AutoUpdatedImage from './AutoUpdatedImage';
|
||||
import { recordingsPageQuery } from '../../pages/RecordingsPage';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@ -50,7 +50,8 @@ const CameraCard = ({
|
||||
navigate(url)
|
||||
}
|
||||
const handleOpenRecordings = () => {
|
||||
throw Error('Not yet implemented')
|
||||
const url = `${routesPath.RECORDINGS_PATH}?${recordingsPageQuery.hostId}=${camera.frigateHost?.id}&${recordingsPageQuery.cameraId}=${camera.id}`
|
||||
navigate(url)
|
||||
}
|
||||
return (
|
||||
<Grid.Col md={6} lg={3} p='0.2rem'>
|
||||
|
||||
@ -7,7 +7,7 @@ import { Modal, createStyles, getStylesRef, rem, Text, Box, Flex, Grid, Divider,
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import KomponentLoader from './CogwheelLoader';
|
||||
import KomponentLoader from './loaders/CogwheelLoader';
|
||||
import ProductParameter from './ProductParameter';
|
||||
import { productString } from '../strings/product.strings';
|
||||
import { IconArrowBadgeLeft, IconArrowBadgeRight } from '@tabler/icons-react';
|
||||
|
||||
25
src/shared/components/RetryError.tsx
Normal file
25
src/shared/components/RetryError.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Center, Text, ActionIcon } from '@mantine/core';
|
||||
import { IconRotateClockwise } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
|
||||
interface RetryErrorProps {
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
const RetryError = ({
|
||||
onRetry
|
||||
}: RetryErrorProps) => {
|
||||
const handleClick = () => {
|
||||
if (onRetry) onRetry()
|
||||
}
|
||||
return (
|
||||
<Center>
|
||||
<Text mr='md'>Loading error</Text>
|
||||
<ActionIcon color="blue" size="md" radius="md" variant="filled">
|
||||
<IconRotateClockwise onClick={handleClick} />
|
||||
</ActionIcon>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default RetryError;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Accordion, Center, Text } from '@mantine/core';
|
||||
import { Accordion, Center, Loader, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -6,20 +6,23 @@ import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services
|
||||
import DayAccordion from './DayAccordion';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Context } from '../../..';
|
||||
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil';
|
||||
import RetryError from '../RetryError';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { RecordSummary } from '../../../types/record';
|
||||
|
||||
interface CameraAccordionProps {
|
||||
camera: GetCameraWHostWConfig,
|
||||
host: GetFrigateHost
|
||||
}
|
||||
|
||||
const CameraAccordion = observer(({
|
||||
const CameraAccordion = ({
|
||||
camera,
|
||||
host
|
||||
}: CameraAccordionProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data, isPending, isError } = useQuery({
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
||||
queryFn: () => {
|
||||
if (camera && host) {
|
||||
@ -44,29 +47,46 @@ const CameraAccordion = observer(({
|
||||
setOpenedDay(value)
|
||||
}
|
||||
|
||||
if (isPending) return <Center><Text>Loading...</Text></Center>
|
||||
if (isError) return <Center><Text>Loading error</Text></Center>
|
||||
if (isPending) return <Center><Loader /></Center>
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
|
||||
if (!data || !camera) return null
|
||||
|
||||
|
||||
const days = data.slice(0, 2).map(rec => (
|
||||
<Accordion.Item key={rec.day} value={rec.day}>
|
||||
<Accordion.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
|
||||
<Accordion.Panel key={rec.day + 'panel'}>
|
||||
<DayAccordion key={rec.day + 'day'} recordSummary={rec} />
|
||||
const recodItem = (record: RecordSummary) => (
|
||||
<Accordion.Item key={record.day} value={record.day}>
|
||||
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control>
|
||||
<Accordion.Panel key={record.day + 'panel'}>
|
||||
<DayAccordion key={record.day + 'day'} recordSummary={record} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)
|
||||
|
||||
))
|
||||
const days = () => {
|
||||
const [startDate, endDate] = recStore.selectedRange
|
||||
if (startDate && endDate) {
|
||||
return data
|
||||
.filter(rec => {
|
||||
const parsedRecDate = parseQueryDateToDate(rec.day)
|
||||
if (parsedRecDate) {
|
||||
return parsedRecDate >= startDate && parsedRecDate <= endDate
|
||||
}
|
||||
return false
|
||||
})
|
||||
.map(rec => recodItem(rec))
|
||||
}
|
||||
if ((startDate && endDate) || (!startDate && !endDate)) {
|
||||
return data.map(rec => recodItem(rec))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('CameraAccordion rendered')
|
||||
|
||||
return (
|
||||
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
||||
{days}
|
||||
{days()}
|
||||
</Accordion>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default CameraAccordion;
|
||||
export default observer(CameraAccordion);
|
||||
@ -1,25 +1,23 @@
|
||||
import { Accordion, Center, Flex, Group, Text } from '@mantine/core';
|
||||
import { Accordion, Center, Group, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { RecordHour, RecordSummary, Recording } from '../../../types/record';
|
||||
import Button from '../frigate/Button';
|
||||
import { IconPlayerPause, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
|
||||
import { RecordSummary } from '../../../types/record';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import PlayControl from './PlayControl';
|
||||
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { Context } from '../../..';
|
||||
import VideoPlayer from '../frigate/VideoPlayer';
|
||||
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||
import EventsAccordion from './EventsAccordion';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import { getResolvedTimeZone } from '../../utils/dateUtil';
|
||||
import DayEventsAccordion from './DayEventsAccordion';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface RecordingAccordionProps {
|
||||
recordSummary?: RecordSummary
|
||||
}
|
||||
|
||||
const DayAccordion = observer(({
|
||||
const DayAccordion = ({
|
||||
recordSummary
|
||||
}: RecordingAccordionProps) => {
|
||||
const { recordingsStore } = useContext(Context)
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
||||
const [openedValue, setOpenedValue] = useState<string>()
|
||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||
@ -28,11 +26,11 @@ const DayAccordion = observer(({
|
||||
if (openVideoPlayer) {
|
||||
console.log('openVideoPlayer', openVideoPlayer)
|
||||
if (openVideoPlayer) {
|
||||
recordingsStore.recordToPlay.day = recordSummary?.day
|
||||
recordingsStore.recordToPlay.hour = openVideoPlayer
|
||||
recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||
const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay)
|
||||
console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay)
|
||||
recStore.recordToPlay.day = recordSummary?.day
|
||||
recStore.recordToPlay.hour = openVideoPlayer
|
||||
recStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||
const parsed = recStore.getFullRecordForPlay(recStore.recordToPlay)
|
||||
console.log('recordingsStore.playedRecord: ', recStore.recordToPlay)
|
||||
if (parsed.success) {
|
||||
const url = proxyApi.recordingURL(
|
||||
parsed.data.hostName,
|
||||
@ -50,10 +48,10 @@ const DayAccordion = observer(({
|
||||
}
|
||||
}, [openVideoPlayer])
|
||||
|
||||
if (!recordSummary || recordSummary.hours.length < 1) return (<Text>Not have record at that day</Text>)
|
||||
if (!recordSummary ) return <Text>Not have record at that day</Text>
|
||||
if (recordSummary.hours.length < 1) return <Text>Not have record at that day</Text>
|
||||
|
||||
const handleOpenPlayer = (hour: string) => {
|
||||
// console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`)
|
||||
if (openVideoPlayer !== hour) {
|
||||
setOpenedValue(hour)
|
||||
setOpenVideoPlayer(hour)
|
||||
@ -73,6 +71,17 @@ const DayAccordion = observer(({
|
||||
|
||||
console.log('DayAccordion rendered')
|
||||
|
||||
const hourLabel = (hour: string, eventsQty: number) => (
|
||||
<Group>
|
||||
<Text>{strings.hour}: {hour}:00</Text>
|
||||
{eventsQty > 0 ?
|
||||
<Text>{strings.events}: {eventsQty}</Text>
|
||||
:
|
||||
<Text>{strings.notHaveEvents}</Text>
|
||||
}
|
||||
</Group>
|
||||
)
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
key={recordSummary.day}
|
||||
@ -81,23 +90,27 @@ const DayAccordion = observer(({
|
||||
value={openedValue}
|
||||
onChange={handleClick}
|
||||
>
|
||||
{recordSummary.hours.slice(0, 5).map(hour => (
|
||||
{recordSummary.hours.map(hour => (
|
||||
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
||||
<Accordion.Control key={hour.hour + 'Control'}>
|
||||
<PlayControl label={`Hour ${hour.hour}`} value={hour.hour} openVideoPlayer={openVideoPlayer} onClick={handleOpenPlayer} />
|
||||
<PlayControl
|
||||
label={hourLabel(hour.hour, hour.events)}
|
||||
value={hour.hour}
|
||||
openVideoPlayer={openVideoPlayer}
|
||||
onClick={handleOpenPlayer} />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel key={hour.hour + 'Panel'}>
|
||||
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
{hour.events > 0 ?
|
||||
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
|
||||
:
|
||||
<Center><Text>Not have events</Text></Center>
|
||||
<Center><Text>{strings.notHaveEvents}</Text></Center>
|
||||
}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default DayAccordion;
|
||||
export default observer(DayAccordion);
|
||||
@ -1,5 +1,6 @@
|
||||
import { Accordion, Text } from '@mantine/core';
|
||||
import React, { Suspense, lazy, useState } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
const EventsAccordion = lazy(() => import('./EventsAccordion'))
|
||||
|
||||
interface DayEventsAccordionProps {
|
||||
@ -21,7 +22,7 @@ const DayEventsAccordion = ({
|
||||
return (
|
||||
<Accordion onChange={handleClick}>
|
||||
<Accordion.Item value={hour}>
|
||||
<Accordion.Control><Text>Events {qty}</Text></Accordion.Control>
|
||||
<Accordion.Control><Text>{strings.events}: {qty}</Text></Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
{openedItem === hour ?
|
||||
<Suspense>
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { Accordion, Center, Text } from '@mantine/core';
|
||||
import { Accordion, Center, Group, Loader, Text } from '@mantine/core';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateQueryKeys, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
||||
import PlayControl from './PlayControl';
|
||||
import VideoPlayer from '../frigate/VideoPlayer';
|
||||
import { formatUnixTimestampToDateTime, getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../frigate/dateUtil';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import { getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../../utils/dateUtil';
|
||||
import RetryError from '../RetryError';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { EventFrigate } from '../../../types/event';
|
||||
|
||||
/**
|
||||
* @param day frigate format, e.g day: 2024-02-23
|
||||
@ -31,8 +34,6 @@ interface EventsAccordionProps {
|
||||
const EventsAccordion = observer(({
|
||||
day,
|
||||
hour,
|
||||
cameraName,
|
||||
hostName,
|
||||
// TODO labels, score
|
||||
}: EventsAccordionProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
@ -40,17 +41,17 @@ const EventsAccordion = observer(({
|
||||
const [openedValue, setOpenedValue] = useState<string>()
|
||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||
|
||||
const inHostName = hostName || recStore.recordToPlay.hostName
|
||||
const inCameraName = cameraName || recStore.recordToPlay.cameraName
|
||||
const isRequiredParams = inCameraName && inHostName
|
||||
const inHost = recStore.selectedHost
|
||||
const inCamera = recStore.selectedCamera
|
||||
const isRequiredParams = inHost && inCamera
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getEvents, day, hour, inCameraName, inHostName],
|
||||
queryKey: [frigateQueryKeys.getEvents, inHost, inCamera, day, hour],
|
||||
queryFn: () => {
|
||||
if (!isRequiredParams) return null
|
||||
const [startTime, endTime] = getUnixTime(day, hour)
|
||||
const parsed = getEventsQuerySchema.safeParse({
|
||||
hostName: inHostName,
|
||||
camerasName: [inCameraName],
|
||||
hostName: mapHostToHostname(inHost),
|
||||
camerasName: [inCamera.name],
|
||||
after: startTime,
|
||||
before: endTime,
|
||||
hasClip: true,
|
||||
@ -78,8 +79,8 @@ const EventsAccordion = observer(({
|
||||
useEffect(() => {
|
||||
if (openVideoPlayer) {
|
||||
console.log('openVideoPlayer', openVideoPlayer)
|
||||
if (openVideoPlayer && inHostName) {
|
||||
const url = proxyApi.eventURL(inHostName, openVideoPlayer)
|
||||
if (openVideoPlayer && inHost) {
|
||||
const url = proxyApi.eventURL(mapHostToHostname(inHost), openVideoPlayer)
|
||||
console.log('GET EVENT URL: ', url)
|
||||
setPlayerUrl(url)
|
||||
}
|
||||
@ -88,8 +89,8 @@ const EventsAccordion = observer(({
|
||||
}
|
||||
}, [openVideoPlayer])
|
||||
|
||||
if (isPending) return <Center><Text>Loading...</Text></Center>
|
||||
if (isError) return <Center><Text>Loading error</Text></Center>
|
||||
if (isPending) return <Center><Loader /></Center>
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
||||
|
||||
const handleOpenPlayer = (eventId: string) => {
|
||||
@ -111,6 +112,20 @@ const EventsAccordion = observer(({
|
||||
setOpenVideoPlayer(undefined)
|
||||
}
|
||||
|
||||
const eventLabel = (event: EventFrigate) => {
|
||||
const time = unixTimeToDate(event.start_time)
|
||||
const duration = getDurationFromTimestamps(event.start_time, event.end_time)
|
||||
return (
|
||||
<Group>
|
||||
<Text>{strings.player.object}: {event.label}</Text>
|
||||
<Text>{time}</Text>
|
||||
{duration ?
|
||||
<Text>{duration}</Text>
|
||||
: <></>}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
variant='separated'
|
||||
@ -118,21 +133,25 @@ const EventsAccordion = observer(({
|
||||
value={openedValue}
|
||||
onChange={handleClick}
|
||||
>
|
||||
{data.slice(0, 5).map(event => (
|
||||
{data.map(event => (
|
||||
<Accordion.Item key={event.id + 'Item'} value={event.id}>
|
||||
<Accordion.Control key={event.id + 'Control'}>
|
||||
<PlayControl
|
||||
label={unixTimeToDate(event.start_time)}
|
||||
label={eventLabel(event)}
|
||||
value={event.id}
|
||||
openVideoPlayer={openVideoPlayer}
|
||||
onClick={handleOpenPlayer} />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel key={event.id + 'Panel'}>
|
||||
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
<Text>Camera: {event.camera}</Text>
|
||||
<Text>Label: {event.label}</Text>
|
||||
<Text>Start: {unixTimeToDate(event.start_time)}</Text>
|
||||
<Text>Duration: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
|
||||
<Group mt='1rem'>
|
||||
<Text>{strings.camera}: {event.camera}</Text>
|
||||
<Text>{strings.player.object}: {event.label}</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text>{strings.player.startTime}: {unixTimeToDate(event.start_time)}</Text>
|
||||
<Text>{strings.player.duration}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
|
||||
</Group>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
|
||||
@ -1,9 +1,33 @@
|
||||
import { Flex, Group, Text } from '@mantine/core';
|
||||
import { IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { Flex, Group, Text, createStyles } from '@mantine/core';
|
||||
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
group: {
|
||||
backgroundColor: theme.colors.blue[7],
|
||||
borderRadius: '1rem',
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.3rem',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.fn.darken(theme.colors.blue[7], 0.2),
|
||||
},
|
||||
},
|
||||
text: {
|
||||
color: theme.white,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
iconStop: {
|
||||
color: theme.colors.red[5]
|
||||
},
|
||||
iconPlay: {
|
||||
color: theme.colors.green[5]
|
||||
}
|
||||
}))
|
||||
|
||||
interface PlayControlProps {
|
||||
label: string,
|
||||
label: string | JSX.Element,
|
||||
value: string,
|
||||
openVideoPlayer?: string,
|
||||
onClick?: (value: string) => void
|
||||
@ -15,26 +39,38 @@ const PlayControl = ({
|
||||
openVideoPlayer,
|
||||
onClick
|
||||
}: PlayControlProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleClick = (value: string) => {
|
||||
if (onClick) onClick(value)
|
||||
}
|
||||
return (
|
||||
<Flex justify='space-between'>
|
||||
{label}
|
||||
<Group>
|
||||
<Text onClick={(event) => {
|
||||
<Group className={classes.group}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}}
|
||||
>
|
||||
<Text className={classes.text}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}}>
|
||||
{openVideoPlayer === value ? 'Stop Video' : 'Play Video'}
|
||||
{openVideoPlayer === value ? strings.player.stopVideo : strings.player.startVideo}
|
||||
</Text>
|
||||
{openVideoPlayer === value ?
|
||||
<IconPlayerStop onClick={(event) => {
|
||||
<IconPlayerStopFilled
|
||||
className={classes.iconStop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}} />
|
||||
:
|
||||
<IconPlayerPlay onClick={(event) => {
|
||||
<IconPlayerPlayFilled
|
||||
className={classes.iconPlay}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}} />
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const TestAccordion = ({ camera }: { camera: any }) => {
|
||||
return (
|
||||
<div>
|
||||
TEST ACCORDION {camera.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestAccordion;
|
||||
@ -3,9 +3,11 @@ import React, { useContext, useEffect } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||
import CogwheelLoader from '../CogwheelLoader';
|
||||
import { Center, Text } from '@mantine/core';
|
||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||
import { Center, Loader, Text } from '@mantine/core';
|
||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||
import { strings } from '../../strings/strings';
|
||||
import RetryError from '../RetryError';
|
||||
|
||||
interface CameraSelectFilterProps {
|
||||
selectedHostId: string,
|
||||
@ -16,7 +18,7 @@ const CameraSelectFilter = ({
|
||||
}: CameraSelectFilterProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data, isError, isPending, isSuccess } = useQuery({
|
||||
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId],
|
||||
queryFn: () => frigateApi.getHost(selectedHostId)
|
||||
})
|
||||
@ -30,8 +32,8 @@ const CameraSelectFilter = ({
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
if (isError) return <Center><Text>Loading error!</Text></Center>
|
||||
if (isPending) return <Loader />
|
||||
if (isError) return <RetryError onRetry={refetch}/>
|
||||
if (!data) return null
|
||||
|
||||
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
|
||||
@ -51,7 +53,7 @@ const CameraSelectFilter = ({
|
||||
return (
|
||||
<OneSelectFilter
|
||||
id='frigate-cameras'
|
||||
label='Select camera:'
|
||||
label={strings.selectCamera}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.selectedCamera?.id || ''}
|
||||
defaultValue={recStore.selectedCamera?.id || ''}
|
||||
|
||||
59
src/shared/components/filters.aps/DateRangeSelectFilter.tsx
Normal file
59
src/shared/components/filters.aps/DateRangeSelectFilter.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import { Context } from '../../..';
|
||||
|
||||
interface DateRangeSelectFilterProps {
|
||||
|
||||
}
|
||||
|
||||
const DateRangeSelectFilter = ({
|
||||
|
||||
}: DateRangeSelectFilterProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const handlePick = (value: [Date | null, Date | null]) => {
|
||||
console.log('handlePick',value)
|
||||
recStore.selectedRange = value
|
||||
}
|
||||
|
||||
console.log('DateRangeSelectFilter rendered')
|
||||
return (
|
||||
<Box>
|
||||
<Flex
|
||||
mt='1rem'
|
||||
justify='space-between'>
|
||||
<Text>{strings.selectRange}</Text>
|
||||
</Flex>
|
||||
<DatePickerInput
|
||||
w='100%'
|
||||
mt='1rem'
|
||||
clearable
|
||||
allowSingleDateInRange
|
||||
valueFormat="YYYY-MM-DD"
|
||||
type="range"
|
||||
placeholder={strings.selectRange}
|
||||
mx="auto"
|
||||
maw={400}
|
||||
value={recStore.selectedRange}
|
||||
onChange={handlePick}
|
||||
renderDay={(date) => {
|
||||
const day = date.getDate();
|
||||
const now = new Date().getDate()
|
||||
return (
|
||||
<Indicator size={6} color="red" offset={-5} disabled={day !== now}>
|
||||
<div>{day}</div>
|
||||
</Indicator>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default observer(DateRangeSelectFilter);
|
||||
63
src/shared/components/filters.aps/HostSelectFilter.tsx
Normal file
63
src/shared/components/filters.aps/HostSelectFilter.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { Center, Text } from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { strings } from '../../strings/strings';
|
||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||
import RetryError from '../RetryError';
|
||||
|
||||
const HostSelectFilter = () => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data: hosts, isError, isPending, isSuccess, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHosts],
|
||||
queryFn: frigateApi.getHosts
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!hosts) return
|
||||
if (recStore.hostIdParam) {
|
||||
recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam)
|
||||
recStore.hostIdParam = undefined
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
if (isError) return <RetryError onRetry={refetch}/>
|
||||
|
||||
if (!hosts || hosts.length < 1) return null
|
||||
|
||||
const hostItems: OneSelectItem[] = hosts
|
||||
.filter(host => host.enabled)
|
||||
.map(host => ({ value: host.id, label: host.name }))
|
||||
|
||||
const handleSelect = (id: string, value: string) => {
|
||||
const host = hosts?.find(host => host.id === value)
|
||||
if (!host) {
|
||||
recStore.selectedHost = undefined
|
||||
recStore.selectedCamera = undefined
|
||||
return
|
||||
}
|
||||
if (recStore.selectedHost?.id !== host.id) {
|
||||
recStore.selectedCamera = undefined
|
||||
}
|
||||
recStore.selectedHost = host
|
||||
}
|
||||
|
||||
return (
|
||||
<OneSelectFilter
|
||||
id='frigate-hosts'
|
||||
label={strings.selectHost}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.selectedHost?.id || ''}
|
||||
defaultValue={recStore.selectedHost?.id || ''}
|
||||
data={hostItems}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(HostSelectFilter);
|
||||
@ -1,97 +1,99 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import CameraImage from "./CameraImage";
|
||||
import { CameraConfig } from "../../../types/frigateConfig";
|
||||
import { useDocumentVisibility } from "@mantine/hooks";
|
||||
import { AspectRatio, Flex } from "@mantine/core";
|
||||
// import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
// import CameraImage from "./CameraImage";
|
||||
// import { CameraConfig } from "../../../types/frigateConfig";
|
||||
// import { useDocumentVisibility } from "@mantine/hooks";
|
||||
// import { AspectRatio, Flex } from "@mantine/core";
|
||||
|
||||
interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
cameraConfig?: CameraConfig
|
||||
searchParams?: {};
|
||||
showFps?: boolean;
|
||||
className?: string;
|
||||
url: string
|
||||
};
|
||||
export {}
|
||||
|
||||
// TODO Delete
|
||||
export default function AutoUpdatingCameraImage({
|
||||
cameraConfig,
|
||||
searchParams = "",
|
||||
showFps = true,
|
||||
className,
|
||||
url,
|
||||
...rest
|
||||
}: AutoUpdatingCameraImageProps) {
|
||||
const [key, setKey] = useState(Date.now());
|
||||
const [fps, setFps] = useState<string>("0");
|
||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||
// interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
// cameraConfig?: CameraConfig
|
||||
// searchParams?: {};
|
||||
// showFps?: boolean;
|
||||
// className?: string;
|
||||
// url: string
|
||||
// };
|
||||
|
||||
const windowVisible = useDocumentVisibility()
|
||||
// // TODO Delete
|
||||
// export default function AutoUpdatingCameraImage({
|
||||
// cameraConfig,
|
||||
// searchParams = "",
|
||||
// showFps = true,
|
||||
// className,
|
||||
// url,
|
||||
// ...rest
|
||||
// }: AutoUpdatingCameraImageProps) {
|
||||
// const [key, setKey] = useState(Date.now());
|
||||
// const [fps, setFps] = useState<string>("0");
|
||||
// const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||
|
||||
// const windowVisible = useDocumentVisibility()
|
||||
|
||||
|
||||
const reloadInterval = useMemo(() => {
|
||||
if (windowVisible === "hidden") {
|
||||
return -1; // no reason to update the image when the window is not visible
|
||||
}
|
||||
|
||||
// if (liveReady) {
|
||||
// return 60000;
|
||||
// const reloadInterval = useMemo(() => {
|
||||
// if (windowVisible === "hidden") {
|
||||
// return -1; // no reason to update the image when the window is not visible
|
||||
// }
|
||||
|
||||
// if (cameraActive) {
|
||||
// return 200;
|
||||
// // if (liveReady) {
|
||||
// // return 60000;
|
||||
// // }
|
||||
|
||||
// // if (cameraActive) {
|
||||
// // return 200;
|
||||
// // }
|
||||
|
||||
// return 30000;
|
||||
// }, [windowVisible]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reloadInterval == -1) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
return 30000;
|
||||
}, [windowVisible]);
|
||||
// setKey(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (reloadInterval == -1) {
|
||||
return;
|
||||
}
|
||||
// return () => {
|
||||
// if (timeoutId) {
|
||||
// clearTimeout(timeoutId);
|
||||
// setTimeoutId(undefined);
|
||||
// }
|
||||
// };
|
||||
// }, [reloadInterval]);
|
||||
|
||||
setKey(Date.now());
|
||||
// const handleLoad = useCallback(() => {
|
||||
// if (reloadInterval == -1) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
setTimeoutId(undefined);
|
||||
}
|
||||
};
|
||||
}, [reloadInterval]);
|
||||
// const loadTime = Date.now() - key;
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
if (reloadInterval == -1) {
|
||||
return;
|
||||
}
|
||||
// if (showFps) {
|
||||
// setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
|
||||
// }
|
||||
|
||||
const loadTime = Date.now() - key;
|
||||
// setTimeoutId(
|
||||
// setTimeout(
|
||||
// () => {
|
||||
// setKey(Date.now());
|
||||
// },
|
||||
// loadTime > reloadInterval ? 1 : reloadInterval
|
||||
// )
|
||||
// );
|
||||
// }, [key, setFps]);
|
||||
|
||||
if (showFps) {
|
||||
setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
|
||||
}
|
||||
|
||||
setTimeoutId(
|
||||
setTimeout(
|
||||
() => {
|
||||
setKey(Date.now());
|
||||
},
|
||||
loadTime > reloadInterval ? 1 : reloadInterval
|
||||
)
|
||||
);
|
||||
}, [key, setFps]);
|
||||
|
||||
return (
|
||||
// <AspectRatio ratio={1}>
|
||||
<Flex direction='column' h='100%'>
|
||||
{/* <CameraImage
|
||||
cameraConfig={cameraConfig}
|
||||
onload={handleLoad}
|
||||
enabled={cameraConfig?.enabled}
|
||||
url={url}
|
||||
{...rest}
|
||||
/> */}
|
||||
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||
</Flex>
|
||||
// </AspectRatio >
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// // <AspectRatio ratio={1}>
|
||||
// <Flex direction='column' h='100%'>
|
||||
// {/* <CameraImage
|
||||
// cameraConfig={cameraConfig}
|
||||
// onload={handleLoad}
|
||||
// enabled={cameraConfig?.enabled}
|
||||
// url={url}
|
||||
// {...rest}
|
||||
// /> */}
|
||||
// {showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||
// </Flex>
|
||||
// // </AspectRatio >
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,115 +1,117 @@
|
||||
import Tooltip from './Tooltip';
|
||||
import { Fragment, useCallback, useRef, useState } from 'react';
|
||||
// import Tooltip from './Tooltip';
|
||||
// import { Fragment, useCallback, useRef, useState } from 'react';
|
||||
|
||||
const ButtonColors = {
|
||||
blue: {
|
||||
contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300',
|
||||
outlined:
|
||||
'text-blue-500 border-2 border-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
|
||||
text: 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-blue-500 hover:text-blue-200',
|
||||
},
|
||||
red: {
|
||||
contained: 'bg-red-500 focus:bg-red-400 active:bg-red-600 ring-red-300',
|
||||
outlined:
|
||||
'text-red-500 border-2 border-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||
text: 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-red-500 hover:text-red-200',
|
||||
},
|
||||
yellow: {
|
||||
contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
|
||||
outlined:
|
||||
'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
text: 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-yellow-500 hover:text-yellow-200',
|
||||
},
|
||||
green: {
|
||||
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
|
||||
outlined:
|
||||
'text-green-500 border-2 border-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
|
||||
text: 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-green-500 hover:text-green-200',
|
||||
},
|
||||
gray: {
|
||||
contained: 'bg-gray-500 focus:bg-gray-400 active:bg-gray-600 ring-gray-300',
|
||||
outlined:
|
||||
'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-gray-500 hover:text-gray-200',
|
||||
},
|
||||
disabled: {
|
||||
contained: 'bg-gray-400',
|
||||
outlined:
|
||||
'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
iconOnly: 'text-gray-500 hover:text-gray-200',
|
||||
},
|
||||
black: {
|
||||
contained: '',
|
||||
outlined: '',
|
||||
text: 'text-black dark:text-white',
|
||||
iconOnly: '',
|
||||
},
|
||||
};
|
||||
export {}
|
||||
|
||||
const ButtonTypes = {
|
||||
contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
|
||||
outlined: '',
|
||||
text: 'transition-opacity',
|
||||
iconOnly: 'transition-opacity',
|
||||
};
|
||||
// const ButtonColors = {
|
||||
// blue: {
|
||||
// contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300',
|
||||
// outlined:
|
||||
// 'text-blue-500 border-2 border-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
|
||||
// text: 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-blue-500 hover:text-blue-200',
|
||||
// },
|
||||
// red: {
|
||||
// contained: 'bg-red-500 focus:bg-red-400 active:bg-red-600 ring-red-300',
|
||||
// outlined:
|
||||
// 'text-red-500 border-2 border-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||
// text: 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-red-500 hover:text-red-200',
|
||||
// },
|
||||
// yellow: {
|
||||
// contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
|
||||
// outlined:
|
||||
// 'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
// text: 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-yellow-500 hover:text-yellow-200',
|
||||
// },
|
||||
// green: {
|
||||
// contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
|
||||
// outlined:
|
||||
// 'text-green-500 border-2 border-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
|
||||
// text: 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-green-500 hover:text-green-200',
|
||||
// },
|
||||
// gray: {
|
||||
// contained: 'bg-gray-500 focus:bg-gray-400 active:bg-gray-600 ring-gray-300',
|
||||
// outlined:
|
||||
// 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
// text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-gray-500 hover:text-gray-200',
|
||||
// },
|
||||
// disabled: {
|
||||
// contained: 'bg-gray-400',
|
||||
// outlined:
|
||||
// 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
// text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
|
||||
// iconOnly: 'text-gray-500 hover:text-gray-200',
|
||||
// },
|
||||
// black: {
|
||||
// contained: '',
|
||||
// outlined: '',
|
||||
// text: 'text-black dark:text-white',
|
||||
// iconOnly: '',
|
||||
// },
|
||||
// };
|
||||
|
||||
export default function Button({
|
||||
children,
|
||||
className = '',
|
||||
color = 'blue',
|
||||
disabled = false,
|
||||
ariaCapitalize = false,
|
||||
href,
|
||||
target,
|
||||
type = 'contained',
|
||||
...attrs
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const ref = useRef();
|
||||
// const ButtonTypes = {
|
||||
// contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
|
||||
// outlined: '',
|
||||
// text: 'transition-opacity',
|
||||
// iconOnly: 'transition-opacity',
|
||||
// };
|
||||
|
||||
let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
|
||||
ButtonColors[disabled ? 'disabled' : color][type]
|
||||
} font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
|
||||
disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer`
|
||||
}`;
|
||||
// export default function Button({
|
||||
// children,
|
||||
// className = '',
|
||||
// color = 'blue',
|
||||
// disabled = false,
|
||||
// ariaCapitalize = false,
|
||||
// href,
|
||||
// target,
|
||||
// type = 'contained',
|
||||
// ...attrs
|
||||
// }) {
|
||||
// const [hovered, setHovered] = useState(false);
|
||||
// const ref = useRef();
|
||||
|
||||
if (disabled) {
|
||||
classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
|
||||
}
|
||||
// let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
|
||||
// ButtonColors[disabled ? 'disabled' : color][type]
|
||||
// } font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
|
||||
// disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer`
|
||||
// }`;
|
||||
|
||||
const handleMousenter = useCallback(() => {
|
||||
setHovered(true);
|
||||
}, []);
|
||||
// if (disabled) {
|
||||
// classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
|
||||
// }
|
||||
|
||||
const handleMouseleave = useCallback(() => {
|
||||
setHovered(false);
|
||||
}, []);
|
||||
// const handleMousenter = useCallback(() => {
|
||||
// setHovered(true);
|
||||
// }, []);
|
||||
|
||||
const Element = href ? 'a' : 'div';
|
||||
// const handleMouseleave = useCallback(() => {
|
||||
// setHovered(false);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Element
|
||||
role="button"
|
||||
aria-disabled={disabled ? 'true' : 'false'}
|
||||
tabindex="0"
|
||||
className={classes}
|
||||
href={href}
|
||||
target={target}
|
||||
ref={ref}
|
||||
onmouseenter={handleMousenter}
|
||||
onmouseleave={handleMouseleave}
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
</Element>
|
||||
{hovered && attrs['aria-label'] ? <Tooltip text={attrs['aria-label']} relativeTo={ref} capitalize={ariaCapitalize} /> : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
// const Element = href ? 'a' : 'div';
|
||||
|
||||
// return (
|
||||
// <Fragment>
|
||||
// <Element
|
||||
// role="button"
|
||||
// aria-disabled={disabled ? 'true' : 'false'}
|
||||
// tabindex="0"
|
||||
// className={classes}
|
||||
// href={href}
|
||||
// target={target}
|
||||
// ref={ref}
|
||||
// onmouseenter={handleMousenter}
|
||||
// onmouseleave={handleMouseleave}
|
||||
// {...attrs}
|
||||
// >
|
||||
// {children}
|
||||
// </Element>
|
||||
// {hovered && attrs['aria-label'] ? <Tooltip text={attrs['aria-label']} relativeTo={ref} capitalize={ariaCapitalize} /> : null}
|
||||
// </Fragment>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,159 +1,161 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
|
||||
import { CameraConfig } from "../../../types/frigateConfig";
|
||||
import { usePersistence } from "../../../hooks/use-persistence";
|
||||
import { Button, Switch, Text } from "@mantine/core";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
||||
import { IconSettings } from "@tabler/icons-react";
|
||||
// import { useCallback, useMemo, useState } from "react";
|
||||
// import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
|
||||
// import { CameraConfig } from "../../../types/frigateConfig";
|
||||
// import { usePersistence } from "../../../hooks/use-persistence";
|
||||
// import { Button, Switch, Text } from "@mantine/core";
|
||||
// import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
||||
// import { IconSettings } from "@tabler/icons-react";
|
||||
|
||||
|
||||
type Options = { [key: string]: boolean };
|
||||
export {}
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
// type Options = { [key: string]: boolean };
|
||||
|
||||
type DebugCameraImageProps = {
|
||||
className?: string;
|
||||
cameraConfig: CameraConfig
|
||||
url: string
|
||||
};
|
||||
// const emptyObject = Object.freeze({});
|
||||
|
||||
export default function DebugCameraImage({
|
||||
className,
|
||||
cameraConfig,
|
||||
url,
|
||||
}: DebugCameraImageProps) {
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [options, setOptions] = usePersistence(
|
||||
`${cameraConfig?.name}-feed`,
|
||||
emptyObject
|
||||
);
|
||||
const handleSetOption = useCallback(
|
||||
(id: string, value: boolean) => {
|
||||
const newOptions = { ...options, [id]: value };
|
||||
setOptions(newOptions);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
const searchParams = useMemo(
|
||||
() =>
|
||||
new URLSearchParams(
|
||||
Object.keys(options).reduce((memo, key) => {
|
||||
//@ts-ignore we know this is correct
|
||||
memo.push([key, options[key] === true ? "1" : "0"]);
|
||||
return memo;
|
||||
}, [])
|
||||
),
|
||||
[options]
|
||||
);
|
||||
const handleToggleSettings = useCallback(() => {
|
||||
setShowSettings(!showSettings);
|
||||
}, [showSettings]);
|
||||
// type DebugCameraImageProps = {
|
||||
// className?: string;
|
||||
// cameraConfig: CameraConfig
|
||||
// url: string
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<AutoUpdatingCameraImage
|
||||
cameraConfig={cameraConfig}
|
||||
searchParams={searchParams}
|
||||
url={url}
|
||||
/>
|
||||
<Button onClick={handleToggleSettings} variant="link" size="sm">
|
||||
<span className="w-5 h-5">
|
||||
<IconSettings />
|
||||
</span>{" "}
|
||||
<span>{showSettings ? "Hide" : "Show"} Options</span>
|
||||
</Button>
|
||||
{showSettings ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DebugSettings
|
||||
handleSetOption={handleSetOption}
|
||||
options={options}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// export default function DebugCameraImage({
|
||||
// className,
|
||||
// cameraConfig,
|
||||
// url,
|
||||
// }: DebugCameraImageProps) {
|
||||
// const [showSettings, setShowSettings] = useState(false);
|
||||
// const [options, setOptions] = usePersistence(
|
||||
// `${cameraConfig?.name}-feed`,
|
||||
// emptyObject
|
||||
// );
|
||||
// const handleSetOption = useCallback(
|
||||
// (id: string, value: boolean) => {
|
||||
// const newOptions = { ...options, [id]: value };
|
||||
// setOptions(newOptions);
|
||||
// },
|
||||
// [options]
|
||||
// );
|
||||
// const searchParams = useMemo(
|
||||
// () =>
|
||||
// new URLSearchParams(
|
||||
// Object.keys(options).reduce((memo, key) => {
|
||||
// //@ts-ignore we know this is correct
|
||||
// memo.push([key, options[key] === true ? "1" : "0"]);
|
||||
// return memo;
|
||||
// }, [])
|
||||
// ),
|
||||
// [options]
|
||||
// );
|
||||
// const handleToggleSettings = useCallback(() => {
|
||||
// setShowSettings(!showSettings);
|
||||
// }, [showSettings]);
|
||||
|
||||
type DebugSettingsProps = {
|
||||
handleSetOption: (id: string, value: boolean) => void;
|
||||
options: Options;
|
||||
};
|
||||
// return (
|
||||
// <div className={className}>
|
||||
// <AutoUpdatingCameraImage
|
||||
// cameraConfig={cameraConfig}
|
||||
// searchParams={searchParams}
|
||||
// url={url}
|
||||
// />
|
||||
// <Button onClick={handleToggleSettings} variant="link" size="sm">
|
||||
// <span className="w-5 h-5">
|
||||
// <IconSettings />
|
||||
// </span>{" "}
|
||||
// <span>{showSettings ? "Hide" : "Show"} Options</span>
|
||||
// </Button>
|
||||
// {showSettings ? (
|
||||
// <Card>
|
||||
// <CardHeader>
|
||||
// <CardTitle>Options</CardTitle>
|
||||
// </CardHeader>
|
||||
// <CardContent>
|
||||
// <DebugSettings
|
||||
// handleSetOption={handleSetOption}
|
||||
// options={options}
|
||||
// />
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="bbox"
|
||||
checked={options["bbox"]}
|
||||
onChange={() => { }}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("bbox", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="bbox">Bounding Box</Label> */}
|
||||
<Text>Bounding Box</Text>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="timestamp"
|
||||
checked={options["timestamp"]}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("timestamp", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="timestamp">Timestamp</Label> */}
|
||||
<Text>Timestamp</Text>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="zones"
|
||||
checked={options["zones"]}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("zones", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="zones">Zones</Label> */}
|
||||
<Text>Zones</Text>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="mask"
|
||||
checked={options["mask"]}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("mask", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="mask">Mask</Label> */}
|
||||
<Text>Mask</Text>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="motion"
|
||||
checked={options["motion"]}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("motion", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="motion">Motion</Label> */}
|
||||
<Text>Motion</Text>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="regions"
|
||||
checked={options["regions"]}
|
||||
// onCheckedChange={(isChecked) => {
|
||||
// handleSetOption("regions", isChecked);
|
||||
// }}
|
||||
/>
|
||||
{/* <Label htmlFor="regions">Regions</Label> */}
|
||||
<Text>Regions</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// type DebugSettingsProps = {
|
||||
// handleSetOption: (id: string, value: boolean) => void;
|
||||
// options: Options;
|
||||
// };
|
||||
|
||||
// function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
// return (
|
||||
// <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="bbox"
|
||||
// checked={options["bbox"]}
|
||||
// onChange={() => { }}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("bbox", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="bbox">Bounding Box</Label> */}
|
||||
// <Text>Bounding Box</Text>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="timestamp"
|
||||
// checked={options["timestamp"]}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("timestamp", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="timestamp">Timestamp</Label> */}
|
||||
// <Text>Timestamp</Text>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="zones"
|
||||
// checked={options["zones"]}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("zones", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="zones">Zones</Label> */}
|
||||
// <Text>Zones</Text>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="mask"
|
||||
// checked={options["mask"]}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("mask", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="mask">Mask</Label> */}
|
||||
// <Text>Mask</Text>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="motion"
|
||||
// checked={options["motion"]}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("motion", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="motion">Motion</Label> */}
|
||||
// <Text>Motion</Text>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Switch
|
||||
// id="regions"
|
||||
// checked={options["regions"]}
|
||||
// // onCheckedChange={(isChecked) => {
|
||||
// // handleSetOption("regions", isChecked);
|
||||
// // }}
|
||||
// />
|
||||
// {/* <Label htmlFor="regions">Regions</Label> */}
|
||||
// <Text>Regions</Text>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,35 +1,37 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { createPortal } from 'preact/compat';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
// import { h, Fragment } from 'preact';
|
||||
// import { createPortal } from 'preact/compat';
|
||||
// import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
export default function Dialog({ children, portalRootID = 'dialogs' }) {
|
||||
const portalRoot = portalRootID && document.getElementById(portalRootID);
|
||||
const [show, setShow] = useState(false);
|
||||
export {}
|
||||
|
||||
useEffect(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
setShow(true);
|
||||
});
|
||||
}, []);
|
||||
// export default function Dialog({ children, portalRootID = 'dialogs' }) {
|
||||
// const portalRoot = portalRootID && document.getElementById(portalRootID);
|
||||
// const [show, setShow] = useState(false);
|
||||
|
||||
const dialog = (
|
||||
<Fragment>
|
||||
<div
|
||||
data-testid="scrim"
|
||||
key="scrim"
|
||||
className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
|
||||
>
|
||||
<div
|
||||
role="modal"
|
||||
className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 sm:max-w-sm md:max-w-md lg:max-w-lg text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
|
||||
show ? 'scale-100 opacity-100' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
// useEffect(() => {
|
||||
// window.requestAnimationFrame(() => {
|
||||
// setShow(true);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
return portalRoot ? createPortal(dialog, portalRoot) : dialog;
|
||||
}
|
||||
// const dialog = (
|
||||
// <Fragment>
|
||||
// <div
|
||||
// data-testid="scrim"
|
||||
// key="scrim"
|
||||
// className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
|
||||
// >
|
||||
// <div
|
||||
// role="modal"
|
||||
// className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 sm:max-w-sm md:max-w-md lg:max-w-lg text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
|
||||
// show ? 'scale-100 opacity-100' : ''
|
||||
// }`}
|
||||
// >
|
||||
// {children}
|
||||
// </div>
|
||||
// </div>
|
||||
// </Fragment>
|
||||
// );
|
||||
|
||||
// return portalRoot ? createPortal(dialog, portalRoot) : dialog;
|
||||
// }
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { h } from 'preact';
|
||||
import { Link as RouterLink } from 'preact-router/match';
|
||||
// import { h } from 'preact';
|
||||
// import { Link as RouterLink } from 'preact-router/match';
|
||||
|
||||
export default function Link({
|
||||
activeClassName = '',
|
||||
className = 'text-blue-500 hover:underline',
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
export {}
|
||||
|
||||
// export default function Link({
|
||||
// activeClassName = '',
|
||||
// className = 'text-blue-500 hover:underline',
|
||||
// children,
|
||||
// href,
|
||||
// ...props
|
||||
// }) {
|
||||
// return (
|
||||
// <RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
|
||||
// {children}
|
||||
// </RouterLink>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,48 +1,50 @@
|
||||
import { h } from 'preact';
|
||||
import RelativeModal from './RelativeModal';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
// import { h } from 'preact';
|
||||
// import RelativeModal from './RelativeModal';
|
||||
// import { useCallback } from 'preact/hooks';
|
||||
|
||||
export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
|
||||
return relativeTo ? (
|
||||
<RelativeModal
|
||||
children={children}
|
||||
className={`${className || ''} py-2`}
|
||||
role="listbox"
|
||||
onDismiss={onDismiss}
|
||||
portalRootID="menus"
|
||||
relativeTo={relativeTo}
|
||||
widthRelative={widthRelative}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
export {}
|
||||
|
||||
export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect && onSelect(value, label);
|
||||
}, [onSelect, value, label]);
|
||||
// export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
|
||||
// return relativeTo ? (
|
||||
// <RelativeModal
|
||||
// children={children}
|
||||
// className={`${className || ''} py-2`}
|
||||
// role="listbox"
|
||||
// onDismiss={onDismiss}
|
||||
// portalRootID="menus"
|
||||
// relativeTo={relativeTo}
|
||||
// widthRelative={widthRelative}
|
||||
// />
|
||||
// ) : null;
|
||||
// }
|
||||
|
||||
const Element = href ? 'a' : 'div';
|
||||
// export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
|
||||
// const handleClick = useCallback(() => {
|
||||
// onSelect && onSelect(value, label);
|
||||
// }, [onSelect, value, label]);
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${
|
||||
focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : ''
|
||||
}`}
|
||||
href={href}
|
||||
onClick={handleClick}
|
||||
role="option"
|
||||
{...attrs}
|
||||
>
|
||||
{Icon ? (
|
||||
<div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
|
||||
<Icon />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="whitespace-nowrap">{label}</div>
|
||||
</Element>
|
||||
);
|
||||
}
|
||||
// const Element = href ? 'a' : 'div';
|
||||
|
||||
export function MenuSeparator() {
|
||||
return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
|
||||
}
|
||||
// return (
|
||||
// <Element
|
||||
// className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${
|
||||
// focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : ''
|
||||
// }`}
|
||||
// href={href}
|
||||
// onClick={handleClick}
|
||||
// role="option"
|
||||
// {...attrs}
|
||||
// >
|
||||
// {Icon ? (
|
||||
// <div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
|
||||
// <Icon />
|
||||
// </div>
|
||||
// ) : null}
|
||||
// <div className="whitespace-nowrap">{label}</div>
|
||||
// </Element>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function MenuSeparator() {
|
||||
// return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
|
||||
// }
|
||||
|
||||
@ -1,70 +1,72 @@
|
||||
import { h } from 'preact';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import Menu from './Menu';
|
||||
import { ArrowDropdown } from '../icons/ArrowDropdown';
|
||||
import Heading from './Heading';
|
||||
import Button from './Button';
|
||||
import SelectOnlyIcon from '../icons/SelectOnly';
|
||||
// import { h } from 'preact';
|
||||
// import { useRef, useState } from 'preact/hooks';
|
||||
// import Menu from './Menu';
|
||||
// import { ArrowDropdown } from '../icons/ArrowDropdown';
|
||||
// import Heading from './Heading';
|
||||
// import Button from './Button';
|
||||
// import SelectOnlyIcon from '../icons/SelectOnly';
|
||||
|
||||
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
||||
const popupRef = useRef(null);
|
||||
export {}
|
||||
|
||||
const [state, setState] = useState({
|
||||
showMenu: false,
|
||||
});
|
||||
// export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
||||
// const popupRef = useRef(null);
|
||||
|
||||
const isOptionSelected = (item) => {
|
||||
return selection == 'all' || selection.split(',').indexOf(item) > -1;
|
||||
};
|
||||
// const [state, setState] = useState({
|
||||
// showMenu: false,
|
||||
// });
|
||||
|
||||
const menuHeight = Math.round(window.innerHeight * 0.55);
|
||||
return (
|
||||
<div className={`${className} p-2`} ref={popupRef}>
|
||||
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
||||
<label>{title}</label>
|
||||
<ArrowDropdown className="w-6" />
|
||||
</div>
|
||||
{state.showMenu ? (
|
||||
<Menu
|
||||
className={`max-h-[${menuHeight}px] overflow-auto`}
|
||||
relativeTo={popupRef}
|
||||
onDismiss={() => setState({ showMenu: false })}
|
||||
>
|
||||
<div className="flex flex-wrap justify-between items-center">
|
||||
<Heading className="p-4 justify-center" size="md">
|
||||
{title}
|
||||
</Heading>
|
||||
<Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
|
||||
Show All
|
||||
</Button>
|
||||
</div>
|
||||
{options.map((item) => (
|
||||
<div className="flex flex-grow" key={item}>
|
||||
<label
|
||||
className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
|
||||
>
|
||||
<input
|
||||
className="mx-4 m-0 align-middle"
|
||||
type="checkbox"
|
||||
checked={isOptionSelected(item)}
|
||||
onChange={() => onToggle(item)}
|
||||
/>
|
||||
{item.replaceAll('_', ' ')}
|
||||
</label>
|
||||
<div className="justify-right">
|
||||
<Button
|
||||
color={isOptionSelected(item) ? 'blue' : 'black'}
|
||||
type="text"
|
||||
className="max-h-[35px] mx-2"
|
||||
onClick={() => onSelectSingle(item)}
|
||||
>
|
||||
{ ( <SelectOnlyIcon /> ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Menu>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// const isOptionSelected = (item) => {
|
||||
// return selection == 'all' || selection.split(',').indexOf(item) > -1;
|
||||
// };
|
||||
|
||||
// const menuHeight = Math.round(window.innerHeight * 0.55);
|
||||
// return (
|
||||
// <div className={`${className} p-2`} ref={popupRef}>
|
||||
// <div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
||||
// <label>{title}</label>
|
||||
// <ArrowDropdown className="w-6" />
|
||||
// </div>
|
||||
// {state.showMenu ? (
|
||||
// <Menu
|
||||
// className={`max-h-[${menuHeight}px] overflow-auto`}
|
||||
// relativeTo={popupRef}
|
||||
// onDismiss={() => setState({ showMenu: false })}
|
||||
// >
|
||||
// <div className="flex flex-wrap justify-between items-center">
|
||||
// <Heading className="p-4 justify-center" size="md">
|
||||
// {title}
|
||||
// </Heading>
|
||||
// <Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
|
||||
// Show All
|
||||
// </Button>
|
||||
// </div>
|
||||
// {options.map((item) => (
|
||||
// <div className="flex flex-grow" key={item}>
|
||||
// <label
|
||||
// className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
|
||||
// >
|
||||
// <input
|
||||
// className="mx-4 m-0 align-middle"
|
||||
// type="checkbox"
|
||||
// checked={isOptionSelected(item)}
|
||||
// onChange={() => onToggle(item)}
|
||||
// />
|
||||
// {item.replaceAll('_', ' ')}
|
||||
// </label>
|
||||
// <div className="justify-right">
|
||||
// <Button
|
||||
// color={isOptionSelected(item) ? 'blue' : 'black'}
|
||||
// type="text"
|
||||
// className="max-h-[35px] mx-2"
|
||||
// onClick={() => onSelectSingle(item)}
|
||||
// >
|
||||
// { ( <SelectOnlyIcon /> ) }
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// </Menu>
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,41 +1,43 @@
|
||||
import { h } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
// import { h } from 'preact';
|
||||
// import { useCallback, useState } from 'preact/hooks';
|
||||
|
||||
export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
|
||||
export {}
|
||||
|
||||
const handleSelected = useCallback(
|
||||
(index) => () => {
|
||||
setSelectedIndex(index);
|
||||
onChange && onChange(index);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
// export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
|
||||
// const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
|
||||
|
||||
const RenderChildren = useCallback(() => {
|
||||
return children.map((child, i) => {
|
||||
child.props.selected = i === selectedIndex;
|
||||
child.props.onClick = handleSelected(i);
|
||||
return child;
|
||||
});
|
||||
}, [selectedIndex, children, handleSelected]);
|
||||
// const handleSelected = useCallback(
|
||||
// (index) => () => {
|
||||
// setSelectedIndex(index);
|
||||
// onChange && onChange(index);
|
||||
// },
|
||||
// [onChange]
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className={`flex ${className}`}>
|
||||
<RenderChildren />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// const RenderChildren = useCallback(() => {
|
||||
// return children.map((child, i) => {
|
||||
// child.props.selected = i === selectedIndex;
|
||||
// child.props.onClick = handleSelected(i);
|
||||
// return child;
|
||||
// });
|
||||
// }, [selectedIndex, children, handleSelected]);
|
||||
|
||||
export function TextTab({ selected, text, onClick, disabled }) {
|
||||
const selectedStyle = disabled
|
||||
? 'text-gray-400 dark:text-gray-600 bg-transparent'
|
||||
: selected
|
||||
? 'text-white bg-blue-500 dark:text-black dark:bg-white'
|
||||
: 'text-black dark:text-white bg-transparent';
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled} className={`rounded-full px-4 py-2 ${selectedStyle}`}>
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <div className={`flex ${className}`}>
|
||||
// <RenderChildren />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function TextTab({ selected, text, onClick, disabled }) {
|
||||
// const selectedStyle = disabled
|
||||
// ? 'text-gray-400 dark:text-gray-600 bg-transparent'
|
||||
// : selected
|
||||
// ? 'text-white bg-blue-500 dark:text-black dark:bg-white'
|
||||
// : 'text-black dark:text-white bg-transparent';
|
||||
// return (
|
||||
// <button onClick={onClick} disabled={disabled} className={`rounded-full px-4 py-2 ${selectedStyle}`}>
|
||||
// <span>{text}</span>
|
||||
// </button>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,83 +1,85 @@
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
// import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface IProp {
|
||||
/** The time to calculate time-ago from */
|
||||
time: Date;
|
||||
/** OPTIONAL: overwrite current time */
|
||||
currentTime?: Date;
|
||||
/** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
|
||||
dense?: boolean;
|
||||
/** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
|
||||
refreshInterval?: number;
|
||||
}
|
||||
// interface IProp {
|
||||
// /** The time to calculate time-ago from */
|
||||
// time: Date;
|
||||
// /** OPTIONAL: overwrite current time */
|
||||
// currentTime?: Date;
|
||||
// /** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
|
||||
// dense?: boolean;
|
||||
// /** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
|
||||
// refreshInterval?: number;
|
||||
// }
|
||||
|
||||
type TimeUnit = {
|
||||
unit: string;
|
||||
full: string;
|
||||
value: number;
|
||||
};
|
||||
export {}
|
||||
|
||||
const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
|
||||
if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
|
||||
// type TimeUnit = {
|
||||
// unit: string;
|
||||
// full: string;
|
||||
// value: number;
|
||||
// };
|
||||
|
||||
const pastTime: Date = new Date(time);
|
||||
const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
|
||||
// const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
|
||||
// if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
|
||||
|
||||
const timeUnits: TimeUnit[] = [
|
||||
{ unit: 'yr', full: 'year', value: 31536000 },
|
||||
{ unit: 'mo', full: 'month', value: 0 },
|
||||
{ unit: 'd', full: 'day', value: 86400 },
|
||||
{ unit: 'h', full: 'hour', value: 3600 },
|
||||
{ unit: 'm', full: 'minute', value: 60 },
|
||||
{ unit: 's', full: 'second', value: 1 },
|
||||
];
|
||||
// const pastTime: Date = new Date(time);
|
||||
// const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
|
||||
|
||||
const elapsed: number = elapsedTime / 1000;
|
||||
if (elapsed < 10) {
|
||||
return 'just now';
|
||||
}
|
||||
// const timeUnits: TimeUnit[] = [
|
||||
// { unit: 'yr', full: 'year', value: 31536000 },
|
||||
// { unit: 'mo', full: 'month', value: 0 },
|
||||
// { unit: 'd', full: 'day', value: 86400 },
|
||||
// { unit: 'h', full: 'hour', value: 3600 },
|
||||
// { unit: 'm', full: 'minute', value: 60 },
|
||||
// { unit: 's', full: 'second', value: 1 },
|
||||
// ];
|
||||
|
||||
for (let i = 0; i < timeUnits.length; i++) {
|
||||
// if months
|
||||
if (i === 1) {
|
||||
// Get the month and year for the time provided
|
||||
const pastMonth = pastTime.getUTCMonth();
|
||||
const pastYear = pastTime.getUTCFullYear();
|
||||
// const elapsed: number = elapsedTime / 1000;
|
||||
// if (elapsed < 10) {
|
||||
// return 'just now';
|
||||
// }
|
||||
|
||||
// get current month and year
|
||||
const currentMonth = currentTime.getUTCMonth();
|
||||
const currentYear = currentTime.getUTCFullYear();
|
||||
// for (let i = 0; i < timeUnits.length; i++) {
|
||||
// // if months
|
||||
// if (i === 1) {
|
||||
// // Get the month and year for the time provided
|
||||
// const pastMonth = pastTime.getUTCMonth();
|
||||
// const pastYear = pastTime.getUTCFullYear();
|
||||
|
||||
let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
|
||||
// // get current month and year
|
||||
// const currentMonth = currentTime.getUTCMonth();
|
||||
// const currentYear = currentTime.getUTCFullYear();
|
||||
|
||||
// check if the time provided is the previous month but not exceeded 1 month ago.
|
||||
if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
|
||||
monthDiff--;
|
||||
}
|
||||
// let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
|
||||
|
||||
if (monthDiff > 0) {
|
||||
const unitAmount = monthDiff;
|
||||
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||
}
|
||||
} else if (elapsed >= timeUnits[i].value) {
|
||||
const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
|
||||
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||
}
|
||||
}
|
||||
return 'Invalid Time';
|
||||
};
|
||||
// // check if the time provided is the previous month but not exceeded 1 month ago.
|
||||
// if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
|
||||
// monthDiff--;
|
||||
// }
|
||||
|
||||
const TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
|
||||
const [currentTime, setCurrentTime] = useState<Date>(new Date());
|
||||
useEffect(() => {
|
||||
const intervalId: NodeJS.Timeout = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, refreshInterval);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [refreshInterval]);
|
||||
// if (monthDiff > 0) {
|
||||
// const unitAmount = monthDiff;
|
||||
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||
// }
|
||||
// } else if (elapsed >= timeUnits[i].value) {
|
||||
// const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
|
||||
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||
// }
|
||||
// }
|
||||
// return 'Invalid Time';
|
||||
// };
|
||||
|
||||
const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
|
||||
// const TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
|
||||
// const [currentTime, setCurrentTime] = useState<Date>(new Date());
|
||||
// useEffect(() => {
|
||||
// const intervalId: NodeJS.Timeout = setInterval(() => {
|
||||
// setCurrentTime(new Date());
|
||||
// }, refreshInterval);
|
||||
// return () => clearInterval(intervalId);
|
||||
// }, [refreshInterval]);
|
||||
|
||||
return <span>{timeAgoValue}</span>;
|
||||
};
|
||||
export default TimeAgo;
|
||||
// const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
|
||||
|
||||
// return <span>{timeAgoValue}</span>;
|
||||
// };
|
||||
// export default TimeAgo;
|
||||
|
||||
@ -1,65 +1,67 @@
|
||||
import { Fragment, h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
// import { Fragment, h } from 'preact';
|
||||
// import { useState } from 'preact/hooks';
|
||||
|
||||
export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
|
||||
const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100);
|
||||
const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100);
|
||||
const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100);
|
||||
const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100);
|
||||
export {}
|
||||
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const getHoverStyle = () => {
|
||||
if (boxLeftEdge < 15) {
|
||||
// show object stats on right side
|
||||
return {
|
||||
left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||
top: `${boxTopEdge}%`,
|
||||
};
|
||||
}
|
||||
// export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
|
||||
// const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100);
|
||||
// const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100);
|
||||
// const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100);
|
||||
// const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100);
|
||||
|
||||
return {
|
||||
right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||
top: `${boxTopEdge}%`,
|
||||
};
|
||||
};
|
||||
// const [isHovering, setIsHovering] = useState(false);
|
||||
// const getHoverStyle = () => {
|
||||
// if (boxLeftEdge < 15) {
|
||||
// // show object stats on right side
|
||||
// return {
|
||||
// left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||
// top: `${boxTopEdge}%`,
|
||||
// };
|
||||
// }
|
||||
|
||||
const getObjectArea = () => {
|
||||
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||
return Math.round(width * height);
|
||||
};
|
||||
// return {
|
||||
// right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||
// top: `${boxTopEdge}%`,
|
||||
// };
|
||||
// };
|
||||
|
||||
const getObjectRatio = () => {
|
||||
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||
return Math.round(100 * (width / height)) / 100;
|
||||
};
|
||||
// const getObjectArea = () => {
|
||||
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||
// return Math.round(width * height);
|
||||
// };
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className="absolute border-4 border-red-600"
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
onTouchStart={() => setIsHovering(true)}
|
||||
onTouchEnd={() => setIsHovering(false)}
|
||||
style={{
|
||||
left: `${boxLeftEdge}%`,
|
||||
top: `${boxTopEdge}%`,
|
||||
right: `${boxRightEdge}%`,
|
||||
bottom: `${boxBottomEdge}%`,
|
||||
}}
|
||||
>
|
||||
{eventOverlay.class_type == 'entered_zone' ? (
|
||||
<div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
|
||||
) : null}
|
||||
</div>
|
||||
{isHovering && (
|
||||
<div className="absolute bg-white dark:bg-slate-800 p-4 block text-black dark:text-white text-lg" style={getHoverStyle()}>
|
||||
<div>{`Area: ${getObjectArea()} px`}</div>
|
||||
<div>{`Ratio: ${getObjectRatio()}`}</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
// const getObjectRatio = () => {
|
||||
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||
// return Math.round(100 * (width / height)) / 100;
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Fragment>
|
||||
// <div
|
||||
// className="absolute border-4 border-red-600"
|
||||
// onMouseEnter={() => setIsHovering(true)}
|
||||
// onMouseLeave={() => setIsHovering(false)}
|
||||
// onTouchStart={() => setIsHovering(true)}
|
||||
// onTouchEnd={() => setIsHovering(false)}
|
||||
// style={{
|
||||
// left: `${boxLeftEdge}%`,
|
||||
// top: `${boxTopEdge}%`,
|
||||
// right: `${boxRightEdge}%`,
|
||||
// bottom: `${boxBottomEdge}%`,
|
||||
// }}
|
||||
// >
|
||||
// {eventOverlay.class_type == 'entered_zone' ? (
|
||||
// <div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
|
||||
// ) : null}
|
||||
// </div>
|
||||
// {isHovering && (
|
||||
// <div className="absolute bg-white dark:bg-slate-800 p-4 block text-black dark:text-white text-lg" style={getHoverStyle()}>
|
||||
// <div>{`Area: ${getObjectArea()} px`}</div>
|
||||
// <div>{`Ratio: ${getObjectRatio()}`}</div>
|
||||
// </div>
|
||||
// )}
|
||||
// </Fragment>
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -1,218 +1,220 @@
|
||||
import { h } from 'preact';
|
||||
import useSWR from 'swr';
|
||||
import ActivityIndicator from './ActivityIndicator';
|
||||
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
||||
import About from '../icons/About';
|
||||
import ActiveObjectIcon from '../icons/ActiveObject';
|
||||
import PlayIcon from '../icons/Play';
|
||||
import ExitIcon from '../icons/Exit';
|
||||
import StationaryObjectIcon from '../icons/StationaryObject';
|
||||
import FaceIcon from '../icons/Face';
|
||||
import LicensePlateIcon from '../icons/LicensePlate';
|
||||
import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
||||
import ZoneIcon from '../icons/Zone';
|
||||
import { useMemo, useState } from 'preact/hooks';
|
||||
import Button from './Button';
|
||||
// import { h } from 'preact';
|
||||
// import useSWR from 'swr';
|
||||
// import ActivityIndicator from './ActivityIndicator';
|
||||
// import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
||||
// import About from '../icons/About';
|
||||
// import ActiveObjectIcon from '../icons/ActiveObject';
|
||||
// import PlayIcon from '../icons/Play';
|
||||
// import ExitIcon from '../icons/Exit';
|
||||
// import StationaryObjectIcon from '../icons/StationaryObject';
|
||||
// import FaceIcon from '../icons/Face';
|
||||
// import LicensePlateIcon from '../icons/LicensePlate';
|
||||
// import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
||||
// import ZoneIcon from '../icons/Zone';
|
||||
// import { useMemo, useState } from 'preact/hooks';
|
||||
// import Button from './Button';
|
||||
|
||||
export default function TimelineSummary({ event, onFrameSelected }) {
|
||||
const { data: eventTimeline } = useSWR([
|
||||
'timeline',
|
||||
{
|
||||
source_id: event.id,
|
||||
},
|
||||
]);
|
||||
export {}
|
||||
|
||||
const { data: config } = useSWR('config');
|
||||
// export default function TimelineSummary({ event, onFrameSelected }) {
|
||||
// const { data: eventTimeline } = useSWR([
|
||||
// 'timeline',
|
||||
// {
|
||||
// source_id: event.id,
|
||||
// },
|
||||
// ]);
|
||||
|
||||
const annotationOffset = useMemo(() => {
|
||||
if (!config) {
|
||||
return 0;
|
||||
}
|
||||
// const { data: config } = useSWR('config');
|
||||
|
||||
return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
|
||||
}, [config, event]);
|
||||
// const annotationOffset = useMemo(() => {
|
||||
// if (!config) {
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
const [timeIndex, setTimeIndex] = useState(-1);
|
||||
// return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
|
||||
// }, [config, event]);
|
||||
|
||||
const recordingParams = useMemo(() => {
|
||||
if (!event.end_time) {
|
||||
return {
|
||||
after: event.start_time,
|
||||
};
|
||||
}
|
||||
// const [timeIndex, setTimeIndex] = useState(-1);
|
||||
|
||||
return {
|
||||
before: event.end_time,
|
||||
after: event.start_time,
|
||||
};
|
||||
}, [event]);
|
||||
const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
|
||||
// const recordingParams = useMemo(() => {
|
||||
// if (!event.end_time) {
|
||||
// return {
|
||||
// after: event.start_time,
|
||||
// };
|
||||
// }
|
||||
|
||||
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||
const getSeekSeconds = (seekUnix) => {
|
||||
if (!recordings) {
|
||||
return 0;
|
||||
}
|
||||
// return {
|
||||
// before: event.end_time,
|
||||
// after: event.start_time,
|
||||
// };
|
||||
// }, [event]);
|
||||
// const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
|
||||
|
||||
let seekSeconds = 0;
|
||||
recordings.every((segment) => {
|
||||
// if the next segment is past the desired time, stop calculating
|
||||
if (segment.start_time > seekUnix) {
|
||||
return false;
|
||||
}
|
||||
// // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||
// const getSeekSeconds = (seekUnix) => {
|
||||
// if (!recordings) {
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
if (segment.end_time < seekUnix) {
|
||||
seekSeconds += segment.end_time - segment.start_time;
|
||||
return true;
|
||||
}
|
||||
// let seekSeconds = 0;
|
||||
// recordings.every((segment) => {
|
||||
// // if the next segment is past the desired time, stop calculating
|
||||
// if (segment.start_time > seekUnix) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
seekSeconds += segment.end_time - segment.start_time - (segment.end_time - seekUnix);
|
||||
return true;
|
||||
});
|
||||
// if (segment.end_time < seekUnix) {
|
||||
// seekSeconds += segment.end_time - segment.start_time;
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return seekSeconds;
|
||||
};
|
||||
// seekSeconds += segment.end_time - segment.start_time - (segment.end_time - seekUnix);
|
||||
// return true;
|
||||
// });
|
||||
|
||||
const onSelectMoment = async (index) => {
|
||||
setTimeIndex(index);
|
||||
onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
|
||||
};
|
||||
// return seekSeconds;
|
||||
// };
|
||||
|
||||
if (!eventTimeline || !config) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
// const onSelectMoment = async (index) => {
|
||||
// setTimeIndex(index);
|
||||
// onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
|
||||
// };
|
||||
|
||||
if (eventTimeline.length == 0) {
|
||||
return <div />;
|
||||
}
|
||||
// if (!eventTimeline || !config) {
|
||||
// return <ActivityIndicator />;
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="h-14 flex justify-center">
|
||||
<div className="flex flex-row flex-nowrap justify-between overflow-auto">
|
||||
{eventTimeline.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
className="rounded-full"
|
||||
type="iconOnly"
|
||||
color={index == timeIndex ? 'blue' : 'gray'}
|
||||
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
||||
onClick={() => onSelectMoment(index)}
|
||||
>
|
||||
{getTimelineIcon(item)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{timeIndex >= 0 ? (
|
||||
<div className="m-2 max-w-md self-center">
|
||||
<div className="flex justify-start">
|
||||
<div className="text-lg flex justify-between py-4">Bounding boxes may not align</div>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
type="text"
|
||||
color="gray"
|
||||
aria-label=" Disclaimer: This data comes from the detect feed but is shown on the recordings, it is unlikely that the
|
||||
streams are perfectly in sync so the bounding box and the footage will not line up perfectly. The annotation_offset field can be used to adjust this."
|
||||
>
|
||||
<About className="w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (eventTimeline.length == 0) {
|
||||
// return <div />;
|
||||
// }
|
||||
|
||||
function getTimelineIcon(timelineItem) {
|
||||
switch (timelineItem.class_type) {
|
||||
case 'visible':
|
||||
return <PlayIcon className="w-8" />;
|
||||
case 'gone':
|
||||
return <ExitIcon className="w-8" />;
|
||||
case 'active':
|
||||
return <ActiveObjectIcon className="w-8" />;
|
||||
case 'stationary':
|
||||
return <StationaryObjectIcon className="w-8" />;
|
||||
case 'entered_zone':
|
||||
return <ZoneIcon className="w-8" />;
|
||||
case 'attribute':
|
||||
switch (timelineItem.data.attribute) {
|
||||
case 'face':
|
||||
return <FaceIcon className="w-8" />;
|
||||
case 'license_plate':
|
||||
return <LicensePlateIcon className="w-8" />;
|
||||
default:
|
||||
return <DeliveryTruckIcon className="w-8" />;
|
||||
}
|
||||
case 'sub_label':
|
||||
switch (timelineItem.data.label) {
|
||||
case 'person':
|
||||
return <FaceIcon className="w-8" />;
|
||||
case 'car':
|
||||
return <LicensePlateIcon className="w-8" />;
|
||||
}
|
||||
}
|
||||
}
|
||||
// return (
|
||||
// <div className="flex flex-col">
|
||||
// <div className="h-14 flex justify-center">
|
||||
// <div className="flex flex-row flex-nowrap justify-between overflow-auto">
|
||||
// {eventTimeline.map((item, index) => (
|
||||
// <Button
|
||||
// key={index}
|
||||
// className="rounded-full"
|
||||
// type="iconOnly"
|
||||
// color={index == timeIndex ? 'blue' : 'gray'}
|
||||
// aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
||||
// onClick={() => onSelectMoment(index)}
|
||||
// >
|
||||
// {getTimelineIcon(item)}
|
||||
// </Button>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// {timeIndex >= 0 ? (
|
||||
// <div className="m-2 max-w-md self-center">
|
||||
// <div className="flex justify-start">
|
||||
// <div className="text-lg flex justify-between py-4">Bounding boxes may not align</div>
|
||||
// <Button
|
||||
// className="rounded-full"
|
||||
// type="text"
|
||||
// color="gray"
|
||||
// aria-label=" Disclaimer: This data comes from the detect feed but is shown on the recordings, it is unlikely that the
|
||||
// streams are perfectly in sync so the bounding box and the footage will not line up perfectly. The annotation_offset field can be used to adjust this."
|
||||
// >
|
||||
// <About className="w-4" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
function getTimelineItemDescription(config, timelineItem, event) {
|
||||
switch (timelineItem.class_type) {
|
||||
case 'visible':
|
||||
return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
})}`;
|
||||
case 'entered_zone':
|
||||
return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
||||
.join(' and ')
|
||||
.replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
})}`;
|
||||
case 'active':
|
||||
return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
})}`;
|
||||
case 'stationary':
|
||||
return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
})}`;
|
||||
case 'attribute': {
|
||||
let title = "";
|
||||
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
|
||||
title = `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label}`;
|
||||
} else {
|
||||
title = `${event.label} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`
|
||||
}
|
||||
return `${title} at ${formatUnixTimestampToDateTime(
|
||||
timelineItem.timestamp,
|
||||
{
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
}
|
||||
)}`;
|
||||
}
|
||||
case 'sub_label':
|
||||
return `${event.label} recognized as ${timelineItem.data.sub_label} at ${formatUnixTimestampToDateTime(
|
||||
timelineItem.timestamp,
|
||||
{
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
}
|
||||
)}`;
|
||||
case 'gone':
|
||||
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
date_style: 'short',
|
||||
time_style: 'medium',
|
||||
time_format: config.ui.time_format,
|
||||
})}`;
|
||||
}
|
||||
}
|
||||
// function getTimelineIcon(timelineItem) {
|
||||
// switch (timelineItem.class_type) {
|
||||
// case 'visible':
|
||||
// return <PlayIcon className="w-8" />;
|
||||
// case 'gone':
|
||||
// return <ExitIcon className="w-8" />;
|
||||
// case 'active':
|
||||
// return <ActiveObjectIcon className="w-8" />;
|
||||
// case 'stationary':
|
||||
// return <StationaryObjectIcon className="w-8" />;
|
||||
// case 'entered_zone':
|
||||
// return <ZoneIcon className="w-8" />;
|
||||
// case 'attribute':
|
||||
// switch (timelineItem.data.attribute) {
|
||||
// case 'face':
|
||||
// return <FaceIcon className="w-8" />;
|
||||
// case 'license_plate':
|
||||
// return <LicensePlateIcon className="w-8" />;
|
||||
// default:
|
||||
// return <DeliveryTruckIcon className="w-8" />;
|
||||
// }
|
||||
// case 'sub_label':
|
||||
// switch (timelineItem.data.label) {
|
||||
// case 'person':
|
||||
// return <FaceIcon className="w-8" />;
|
||||
// case 'car':
|
||||
// return <LicensePlateIcon className="w-8" />;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// function getTimelineItemDescription(config, timelineItem, event) {
|
||||
// switch (timelineItem.class_type) {
|
||||
// case 'visible':
|
||||
// return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// })}`;
|
||||
// case 'entered_zone':
|
||||
// return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
||||
// .join(' and ')
|
||||
// .replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// })}`;
|
||||
// case 'active':
|
||||
// return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// })}`;
|
||||
// case 'stationary':
|
||||
// return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// })}`;
|
||||
// case 'attribute': {
|
||||
// let title = "";
|
||||
// if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
|
||||
// title = `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label}`;
|
||||
// } else {
|
||||
// title = `${event.label} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`
|
||||
// }
|
||||
// return `${title} at ${formatUnixTimestampToDateTime(
|
||||
// timelineItem.timestamp,
|
||||
// {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// }
|
||||
// )}`;
|
||||
// }
|
||||
// case 'sub_label':
|
||||
// return `${event.label} recognized as ${timelineItem.data.sub_label} at ${formatUnixTimestampToDateTime(
|
||||
// timelineItem.timestamp,
|
||||
// {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// }
|
||||
// )}`;
|
||||
// case 'gone':
|
||||
// return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||
// date_style: 'short',
|
||||
// time_style: 'medium',
|
||||
// time_format: config.ui.time_format,
|
||||
// })}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -1,62 +1,64 @@
|
||||
import { createPortal } from 'react'; // TODO implement
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
// import { createPortal } from 'react'; // TODO implement
|
||||
// import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
const TIP_SPACE = 20;
|
||||
// const TIP_SPACE = 20;
|
||||
|
||||
export default function Tooltip({ relativeTo, text, capitalize }) {
|
||||
const [position, setPosition] = useState({ top: -9999, left: -9999 });
|
||||
const portalRoot = document.getElementById('tooltips');
|
||||
const ref = useRef();
|
||||
export {}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref && ref.current && relativeTo && relativeTo.current) {
|
||||
const windowWidth = window.innerWidth;
|
||||
const {
|
||||
x: relativeToX,
|
||||
y: relativeToY,
|
||||
width: relativeToWidth,
|
||||
height: relativeToHeight,
|
||||
} = relativeTo.current.getBoundingClientRect();
|
||||
const { width: _tipWidth, height: _tipHeight } = ref.current.getBoundingClientRect();
|
||||
const tipWidth = _tipWidth * 1.1;
|
||||
const tipHeight = _tipHeight * 1.1;
|
||||
// export default function Tooltip({ relativeTo, text, capitalize }) {
|
||||
// const [position, setPosition] = useState({ top: -9999, left: -9999 });
|
||||
// const portalRoot = document.getElementById('tooltips');
|
||||
// const ref = useRef();
|
||||
|
||||
const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
|
||||
const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
|
||||
// useLayoutEffect(() => {
|
||||
// if (ref && ref.current && relativeTo && relativeTo.current) {
|
||||
// const windowWidth = window.innerWidth;
|
||||
// const {
|
||||
// x: relativeToX,
|
||||
// y: relativeToY,
|
||||
// width: relativeToWidth,
|
||||
// height: relativeToHeight,
|
||||
// } = relativeTo.current.getBoundingClientRect();
|
||||
// const { width: _tipWidth, height: _tipHeight } = ref.current.getBoundingClientRect();
|
||||
// const tipWidth = _tipWidth * 1.1;
|
||||
// const tipHeight = _tipHeight * 1.1;
|
||||
|
||||
let newTop = top - TIP_SPACE - tipHeight;
|
||||
let newLeft = left - Math.round(tipWidth / 2);
|
||||
// too far right
|
||||
if (newLeft + tipWidth + TIP_SPACE > windowWidth - window.scrollX) {
|
||||
newLeft = Math.max(0, left - tipWidth - TIP_SPACE);
|
||||
newTop = top - Math.round(tipHeight / 2);
|
||||
}
|
||||
// too far left
|
||||
else if (newLeft < TIP_SPACE + window.scrollX) {
|
||||
newLeft = left + TIP_SPACE;
|
||||
newTop = top - Math.round(tipHeight / 2);
|
||||
}
|
||||
// too close to top
|
||||
else if (newTop <= TIP_SPACE + window.scrollY) {
|
||||
newTop = top + tipHeight + TIP_SPACE;
|
||||
}
|
||||
// const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
|
||||
// const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
|
||||
|
||||
setPosition({ left: newLeft, top: newTop });
|
||||
}
|
||||
}, [relativeTo, ref]);
|
||||
// let newTop = top - TIP_SPACE - tipHeight;
|
||||
// let newLeft = left - Math.round(tipWidth / 2);
|
||||
// // too far right
|
||||
// if (newLeft + tipWidth + TIP_SPACE > windowWidth - window.scrollX) {
|
||||
// newLeft = Math.max(0, left - tipWidth - TIP_SPACE);
|
||||
// newTop = top - Math.round(tipHeight / 2);
|
||||
// }
|
||||
// // too far left
|
||||
// else if (newLeft < TIP_SPACE + window.scrollX) {
|
||||
// newLeft = left + TIP_SPACE;
|
||||
// newTop = top - Math.round(tipHeight / 2);
|
||||
// }
|
||||
// // too close to top
|
||||
// else if (newTop <= TIP_SPACE + window.scrollY) {
|
||||
// newTop = top + tipHeight + TIP_SPACE;
|
||||
// }
|
||||
|
||||
const tooltip = (
|
||||
<div
|
||||
role="tooltip"
|
||||
className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-transform transition-opacity duration-75 transform scale-90 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
|
||||
capitalize ? 'capitalize' : ''
|
||||
} ${position.top >= 0 ? 'opacity-100 scale-100' : ''}`}
|
||||
ref={ref}
|
||||
style={position}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
// setPosition({ left: newLeft, top: newTop });
|
||||
// }
|
||||
// }, [relativeTo, ref]);
|
||||
|
||||
return portalRoot ? createPortal(tooltip, portalRoot) : tooltip;
|
||||
}
|
||||
// const tooltip = (
|
||||
// <div
|
||||
// role="tooltip"
|
||||
// className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-transform transition-opacity duration-75 transform scale-90 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
|
||||
// capitalize ? 'capitalize' : ''
|
||||
// } ${position.top >= 0 ? 'opacity-100 scale-100' : ''}`}
|
||||
// ref={ref}
|
||||
// style={position}
|
||||
// >
|
||||
// {text}
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// return portalRoot ? createPortal(tooltip, portalRoot) : tooltip;
|
||||
// }
|
||||
|
||||
@ -1,80 +1,81 @@
|
||||
import * as React from "react"
|
||||
// import * as React from "react"
|
||||
|
||||
export {}
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// className={cn(
|
||||
// "rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
// className
|
||||
// )}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
// const Card = React.forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <div
|
||||
// ref={ref}
|
||||
// // className={cn(
|
||||
// // "rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
// // className
|
||||
// // )}
|
||||
// {...props}
|
||||
// />
|
||||
// ))
|
||||
// Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
// const CardHeader = React.forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <div
|
||||
// ref={ref}
|
||||
// // className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
// {...props}
|
||||
// />
|
||||
// ))
|
||||
// CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
// className={cn(
|
||||
// "text-2xl font-semibold leading-none tracking-tight",
|
||||
// className
|
||||
// )}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
// const CardTitle = React.forwardRef<
|
||||
// HTMLParagraphElement,
|
||||
// React.HTMLAttributes<HTMLHeadingElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <h3
|
||||
// ref={ref}
|
||||
// // className={cn(
|
||||
// // "text-2xl font-semibold leading-none tracking-tight",
|
||||
// // className
|
||||
// // )}
|
||||
// {...props}
|
||||
// />
|
||||
// ))
|
||||
// CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
// className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
// const CardDescription = React.forwardRef<
|
||||
// HTMLParagraphElement,
|
||||
// React.HTMLAttributes<HTMLParagraphElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <p
|
||||
// ref={ref}
|
||||
// // className={cn("text-sm text-muted-foreground", className)}
|
||||
// {...props}
|
||||
// />
|
||||
// ))
|
||||
// CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref}
|
||||
// className= {cn("p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
// const CardContent = React.forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <div ref={ref}
|
||||
// // className= {cn("p-6 pt-0", className)}
|
||||
// {...props} />
|
||||
// ))
|
||||
// CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
// const CardFooter = React.forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >(({ className, ...props }, ref) => (
|
||||
// <div
|
||||
// ref={ref}
|
||||
// // className={cn("flex items-center p-6 pt-0", className)}
|
||||
// {...props}
|
||||
// />
|
||||
// ))
|
||||
// CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
// export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function About({ className = '' }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={`${className}`}
|
||||
>
|
||||
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(About);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function CalendarIcon({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(CalendarIcon);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Camera({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Camera);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Clip({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Clip);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Clock({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Clock);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Delete({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Delete);
|
||||
@ -1,26 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
|
||||
|
||||
export function Download({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => void }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Download);
|
||||
@ -1,13 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Menu({ className = '' }) {
|
||||
return (
|
||||
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Menu);
|
||||
@ -1,13 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function MenuOpen({ className = '' }) {
|
||||
return (
|
||||
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3 18h13v-2H3v2zm0-5h10v-2H3v2zm0-7v2h13V6H3zm18 9.59L17.42 12 21 8.41 19.59 7l-5 5 5 5L21 15.59z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MenuOpen);
|
||||
@ -1,20 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Score({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'currentColor', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<title>percent</title>
|
||||
<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M19,19H15V21H19A2,2 0 0,0 21,19V15H19M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M5,5H9V3H5A2,2 0 0,0 3,5V9H5M5,15H3V19A2,2 0 0,0 5,21H9V19H5V15Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Score);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Snapshot({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Snapshot);
|
||||
@ -1,24 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function StarRecording({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(StarRecording);
|
||||
@ -1,19 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Submitted({ className = 'h-6 w-6', inner_fill = 'none', outer_stroke = 'currentColor', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
viewBox="0 0 32 32"
|
||||
onClick={onClick}
|
||||
>
|
||||
<rect x="10" y="15" fill={inner_fill} width="12" height="2"/>
|
||||
<rect x="15" y="10" fill={inner_fill} width="2" height="12"/>
|
||||
<circle fill="none" stroke={outer_stroke} stroke-width="2" stroke-miterlimit="10" cx="16" cy="16" r="12"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Submitted);
|
||||
@ -1,23 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function UploadPlus({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(UploadPlus);
|
||||
@ -1,25 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Zone({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
onClick={onClick}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Zone);
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Center, DEFAULT_THEME } from '@mantine/core';
|
||||
import CogwheelSVG from './svg/CogwheelSVG';
|
||||
import CogwheelSVG from '../svg/CogwheelSVG';
|
||||
|
||||
|
||||
const CogwheelLoader = () => {
|
||||
@ -15,7 +15,7 @@ export class RecordingsStore {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
recordingSchema = z.object({
|
||||
private _recordingSchema = z.object({
|
||||
hostName: z.string(),
|
||||
cameraName: z.string(),
|
||||
hour: z.string(),
|
||||
@ -31,7 +31,7 @@ export class RecordingsStore {
|
||||
this._recordToPlay = value
|
||||
}
|
||||
getFullRecordForPlay(value: RecordForPlay) {
|
||||
return this.recordingSchema.safeParse(value)
|
||||
return this._recordingSchema.safeParse(value)
|
||||
}
|
||||
|
||||
private _hostIdParam: string | undefined
|
||||
@ -63,6 +63,11 @@ export class RecordingsStore {
|
||||
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
|
||||
this._selectedCamera = value
|
||||
}
|
||||
selectedStartDay: string = ''
|
||||
selectedEndDay: string = ''
|
||||
private _selectedRange: [Date | null, Date | null] = [null, null]
|
||||
public get selectedRange(): [Date | null, Date | null] {
|
||||
return this._selectedRange
|
||||
}
|
||||
public set selectedRange(value: [Date | null, Date | null]) {
|
||||
this._selectedRange = value
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,31 @@
|
||||
export const strings = {
|
||||
host: {
|
||||
host: 'Хост',
|
||||
hostArr: {
|
||||
name: 'Имя хоста',
|
||||
url: 'Адрес',
|
||||
enabled: 'Включен',
|
||||
},
|
||||
// user section
|
||||
player: {
|
||||
startVideo: 'Вкл Видео',
|
||||
stopVideo: 'Выкл Видео',
|
||||
object: 'Объект',
|
||||
duration: 'Длительность',
|
||||
startTime: 'Начало',
|
||||
endTime: 'Конец'
|
||||
},
|
||||
camera: 'Камера',
|
||||
hour: 'Час',
|
||||
minute: 'Минута',
|
||||
second: 'Секунда',
|
||||
events: 'События',
|
||||
event: 'Событие',
|
||||
notHaveEvents: 'Нет событий',
|
||||
date: 'Дата',
|
||||
day: 'День',
|
||||
selectHost:'Выбери хост',
|
||||
selectCamera: 'Выбери камеру',
|
||||
selectDate: 'Выбери дату',
|
||||
selectRange: 'Выбери период',
|
||||
aboutMe: "Обо мне",
|
||||
settings: "Настройки",
|
||||
changeTheme: "Изменить тему",
|
||||
|
||||
@ -30,6 +30,33 @@ export const unixTimeToDate = (unixTime: number) => {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param date
|
||||
* @returns string '2024-02-25'
|
||||
*/
|
||||
export const dateToQueryString = (date: Date): string => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const formattedMonth = month < 10 ? `0${month}` : month;
|
||||
const day = date.getDate();
|
||||
const formattedDay = day < 10 ? `0${day}` : day;
|
||||
|
||||
return `${year}-${formattedMonth}-${formattedDay}`;
|
||||
}
|
||||
|
||||
export const parseQueryDateToDate = (dateQuery: string): Date | null => {
|
||||
const match = dateQuery.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
|
||||
if (match) {
|
||||
const year = parseInt(match[1], 10);
|
||||
const month = parseInt(match[2], 10) - 1;
|
||||
const day = parseInt(match[3], 10);
|
||||
return new Date(year, month, day);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param day frigate format, e.g day: 2024-02-23
|
||||
* @param hour frigate format, e.g hour: 22
|
||||
@ -210,14 +237,14 @@ interface DurationToken {
|
||||
* @param end_time: number|null - Unix timestamp for end time
|
||||
* @returns string - duration or 'In Progress' if end time is not provided
|
||||
*/
|
||||
export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string => {
|
||||
export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string | undefined => {
|
||||
if (isNaN(start_time)) {
|
||||
return 'Invalid start time';
|
||||
return
|
||||
}
|
||||
let duration = 'In Progress';
|
||||
if (end_time) {
|
||||
if (isNaN(end_time)) {
|
||||
return 'Invalid end time';
|
||||
return
|
||||
}
|
||||
const start = fromUnixTime(start_time);
|
||||
const end = fromUnixTime(end_time);
|
||||
@ -61,9 +61,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
|
||||
}
|
||||
|
||||
const headTitle = [
|
||||
{ propertyName: 'name', title: strings.host.name },
|
||||
{ propertyName: 'host', title: strings.host.url },
|
||||
{ propertyName: 'enabled', title: strings.host.enabled },
|
||||
{ propertyName: 'name', title: strings.hostArr.name },
|
||||
{ propertyName: 'host', title: strings.hostArr.url },
|
||||
{ propertyName: 'enabled', title: strings.hostArr.enabled },
|
||||
{ title: '', sorting: false },
|
||||
]
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import JSMpegPlayer from './JSMpegPlayer';
|
||||
import MSEPlayer from './MsePlayer';
|
||||
import { CameraConfig } from '../../../types/frigateConfig';
|
||||
import { LivePlayerMode } from '../../../types/live';
|
||||
import useCameraActivity from '../../../hooks/use-camera-activity';
|
||||
import useCameraLiveMode from '../../../hooks/use-camera-live-mode';
|
||||
import WebRtcPlayer from './WebRTCPlayer';
|
||||
import JSMpegPlayer from '../shared/components/players/JSMpegPlayer';
|
||||
import MSEPlayer from '../shared/components/players/MsePlayer';
|
||||
import { CameraConfig } from '../types/frigateConfig';
|
||||
import { LivePlayerMode } from '../types/live';
|
||||
import useCameraActivity from '../hooks/use-camera-activity';
|
||||
import useCameraLiveMode from '../hooks/use-camera-live-mode';
|
||||
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer';
|
||||
import { Flex } from '@mantine/core';
|
||||
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../../../services/frigate.proxy/frigate.schema';
|
||||
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
|
||||
type LivePlayerProps = {
|
||||
camera: GetCameraWHostWConfig;
|
||||
@ -70,7 +70,7 @@ const Player = ({
|
||||
player = (
|
||||
<MSEPlayer
|
||||
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
|
||||
camera='Not yet implemented'
|
||||
camera='Not yet implemented' // TODO implement player
|
||||
playbackEnabled={cameraActive}
|
||||
onPlaying={() => setLiveReady(true)}
|
||||
wsUrl={wsUrl}
|
||||
@ -1,12 +1,9 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
|
||||
import React, { useContext } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Context } from '..';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { Center, Text } from '@mantine/core';
|
||||
import CogwheelLoader from '../shared/components/CogwheelLoader';
|
||||
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter';
|
||||
import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter';
|
||||
import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter';
|
||||
|
||||
interface RecordingsFiltersRightSideProps {
|
||||
}
|
||||
@ -15,58 +12,18 @@ const RecordingsFiltersRightSide = ({
|
||||
}: RecordingsFiltersRightSideProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data: hosts, isError, isPending, isSuccess } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHosts],
|
||||
queryFn: frigateApi.getHosts
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!hosts) return
|
||||
if (recStore.hostIdParam) {
|
||||
recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam)
|
||||
recStore.hostIdParam = undefined
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
if (isError) return <Center><Text>Loading error!</Text></Center>
|
||||
|
||||
if (!hosts || hosts.length < 1) return null
|
||||
|
||||
const hostItems: OneSelectItem[] = hosts
|
||||
.filter(host => host.enabled)
|
||||
.map(host => ({ value: host.id, label: host.name }))
|
||||
|
||||
const handleSelect = (id: string, value: string) => {
|
||||
const host = hosts?.find(host => host.id === value)
|
||||
if (!host) {
|
||||
recStore.selectedHost = undefined
|
||||
recStore.selectedCamera = undefined
|
||||
return
|
||||
}
|
||||
if (recStore.selectedHost?.id !== host.id) {
|
||||
recStore.selectedCamera = undefined
|
||||
}
|
||||
recStore.selectedHost = host
|
||||
}
|
||||
|
||||
console.log('RecordingsFiltersRightSide rendered')
|
||||
return (
|
||||
<>
|
||||
<OneSelectFilter
|
||||
id='frigate-hosts'
|
||||
label='Select host:'
|
||||
spaceBetween='1rem'
|
||||
value={recStore.selectedHost?.id || ''}
|
||||
defaultValue={recStore.selectedHost?.id || ''}
|
||||
data={hostItems}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
<HostSelectFilter />
|
||||
{recStore.selectedHost ?
|
||||
<CameraSelectFilter
|
||||
selectedHostId={recStore.selectedHost.id} />
|
||||
:
|
||||
<></>
|
||||
: <></>
|
||||
}
|
||||
{recStore.selectedCamera ?
|
||||
<DateRangeSelectFilter />
|
||||
: <></>
|
||||
}
|
||||
</>
|
||||
|
||||
|
||||
53
src/widgets/SelecteDayList.tsx
Normal file
53
src/widgets/SelecteDayList.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useContext } from 'react';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
|
||||
import { Context } from '..';
|
||||
import { Flex, Text } from '@mantine/core';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import DayAccordion from '../shared/components/accordion/DayAccordion';
|
||||
|
||||
interface SelecteDayListProps {
|
||||
day: Date
|
||||
|
||||
}
|
||||
|
||||
const SelecteDayList = ({
|
||||
day
|
||||
}: SelecteDayListProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const camera = recStore.selectedCamera
|
||||
const host = recStore.selectedHost
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.selectedCamera?.id, day],
|
||||
queryFn: async () => {
|
||||
if (camera && host) {
|
||||
const stringDay = dateToQueryString(day)
|
||||
const hostName = mapHostToHostname(host)
|
||||
const res = await proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
|
||||
return res.find(record => record.day === stringDay)
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const handleRetry = () => {
|
||||
if (recStore.selectedHost) refetch()
|
||||
}
|
||||
|
||||
if (isPending) return <CenterLoader />
|
||||
if (isError) return <RetryErrorPage onRetry={handleRetry} />
|
||||
if (!camera || !host || !data) return <CenterLoader />
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||
<Text>{host.name} / {camera.name} / {data.day}</Text>
|
||||
<DayAccordion recordSummary={data} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(SelecteDayList);
|
||||
@ -6,24 +6,25 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { Context } from '..';
|
||||
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { host } from '../shared/env.const';
|
||||
import CogwheelLoader from '../shared/components/CogwheelLoader';
|
||||
import RetryError from '../pages/RetryError';
|
||||
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
|
||||
interface SelectedCameraListProps {
|
||||
cameraId: string,
|
||||
// cameraId: string,
|
||||
}
|
||||
|
||||
const SelectedCameraList = ({
|
||||
cameraId,
|
||||
// cameraId,
|
||||
}: SelectedCameraListProps) => {
|
||||
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getCameraWHost, cameraId],
|
||||
queryKey: [frigateQueryKeys.getCameraWHost, recStore.selectedCamera?.id],
|
||||
queryFn: async () => {
|
||||
if (cameraId) {
|
||||
return frigateApi.getCameraWHost(cameraId)
|
||||
if (recStore.selectedCamera) {
|
||||
return frigateApi.getCameraWHost(recStore.selectedCamera.id)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -33,14 +34,18 @@ const SelectedCameraList = ({
|
||||
cameraRefetch()
|
||||
}
|
||||
|
||||
if (cameraPending) return <CogwheelLoader />
|
||||
if (cameraError) return <RetryError onRetry={handleRetry} />
|
||||
if (cameraPending) return <CenterLoader />
|
||||
if (cameraError) return <RetryErrorPage onRetry={handleRetry} />
|
||||
|
||||
if (!camera?.frigateHost) return null
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||
<Text>{camera.frigateHost.name} / {camera.name}</Text>
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
<Suspense>
|
||||
<CameraAccordion camera={camera} host={camera.frigateHost} />
|
||||
</Suspense>
|
||||
|
||||
@ -5,7 +5,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { Context } from '..';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from '../pages/RetryError';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
|
||||
|
||||
|
||||
@ -39,14 +40,14 @@ const SelectedHostList = ({
|
||||
}
|
||||
|
||||
if (hostPending) return <CenterLoader />
|
||||
if (hostError) return <RetryError onRetry={handleRetry} />
|
||||
if (hostError) return <RetryErrorPage onRetry={handleRetry} />
|
||||
|
||||
if (!host || host.cameras.length < 1) return null
|
||||
|
||||
const cameras = host.cameras.slice(0, 2).map(camera => {
|
||||
return (
|
||||
<Accordion.Item key={camera.id + 'Item'} value={camera.id}>
|
||||
<Accordion.Control key={camera.id + 'Control'}>{camera.name}</Accordion.Control>
|
||||
<Accordion.Control key={camera.id + 'Control'}>{strings.camera}: {camera.name}</Accordion.Control>
|
||||
<Accordion.Panel key={camera.id + 'Panel'}>
|
||||
{openCameraId === camera.id && (
|
||||
<Suspense>
|
||||
@ -60,7 +61,7 @@ const SelectedHostList = ({
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||
<Text>{host.name}</Text>
|
||||
<Text>{strings.host}: {host.name}</Text>
|
||||
<Accordion
|
||||
mt='1rem'
|
||||
variant='separated'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user