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 { Button, Flex, Text } from '@mantine/core';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
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 { strings } from '../shared/strings/strings';
|
||||||
import { redirect, useNavigate } from 'react-router-dom';
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
import { routesPath } from '../router/routes.path';
|
import { routesPath } from '../router/routes.path';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button, Flex, Text } from '@mantine/core';
|
import { Button, Flex, Text } from '@mantine/core';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
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 { strings } from '../shared/strings/strings';
|
||||||
import { redirect, useNavigate } from 'react-router-dom';
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
import { routesPath } from '../router/routes.path';
|
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 { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
|
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { Button, Flex } from '@mantine/core';
|
import { Button, Flex } from '@mantine/core';
|
||||||
@ -71,7 +71,7 @@ const FrigateHostsPage = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hostsPending) return <CenterLoader />
|
if (hostsPending) return <CenterLoader />
|
||||||
if (hostsError) return <RetryError />
|
if (hostsError) return <RetryErrorPage />
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { configureMonacoYaml } from "monaco-yaml";
|
|||||||
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
|
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
|
|
||||||
|
|
||||||
const HostConfigPage = () => {
|
const HostConfigPage = () => {
|
||||||
@ -74,7 +74,7 @@ const HostConfigPage = () => {
|
|||||||
|
|
||||||
if (configPending) return <CenterLoader />
|
if (configPending) return <CenterLoader />
|
||||||
|
|
||||||
if (configError) return <RetryError onRetry={refetch} />
|
if (configError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction='column' h='100%' w='100%' justify='stretch'>
|
<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 { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import Player from '../shared/components/frigate/Player';
|
import Player from '../widgets/Player';
|
||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
|
|
||||||
const LiveCameraPage = observer(() => {
|
const LiveCameraPage = observer(() => {
|
||||||
@ -28,7 +28,7 @@ const LiveCameraPage = observer(() => {
|
|||||||
|
|
||||||
if (isPending) return <CenterLoader />
|
if (isPending) return <CenterLoader />
|
||||||
|
|
||||||
if (isError) return <RetryError onRetry={refetch} />
|
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w='100%' h='100%' justify='center'>
|
<Flex w='100%' h='100%' justify='center'>
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import { observer } from 'mobx-react-lite'
|
|||||||
import CenterLoader from '../shared/components/CenterLoader';
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import RetryError from './RetryError';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import CameraCard from '../shared/components/CameraCard';
|
import CameraCard from '../shared/components/CameraCard';
|
||||||
|
|
||||||
const MainBody = observer(() => {
|
const MainPage = () => {
|
||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sideBarsStore.rightVisible = false
|
sideBarsStore.rightVisible = false
|
||||||
sideBarsStore.setLeftChildren(null)
|
sideBarsStore.setLeftChildren(null)
|
||||||
@ -32,11 +33,11 @@ const MainBody = observer(() => {
|
|||||||
|
|
||||||
if (isPending) return <CenterLoader />
|
if (isPending) return <CenterLoader />
|
||||||
|
|
||||||
if (isError) return <RetryError onRetry={refetch} />
|
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
const cards = () => {
|
const cards = () => {
|
||||||
// return cameras.filter(cam => cam.frigateHost?.host.includes('5001')).slice(0,1).map(camera => (
|
return cameras.filter(cam => cam.frigateHost?.host.includes('5000')).slice(0,25).map(camera => (
|
||||||
return cameras.map(camera => (
|
// return cameras.map(camera => (
|
||||||
<CameraCard
|
<CameraCard
|
||||||
key={camera.id}
|
key={camera.id}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
@ -70,6 +71,6 @@ const MainBody = observer(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
|
|
||||||
export default MainBody;
|
export default observer(MainPage);
|
||||||
@ -9,12 +9,18 @@ import SelectedCameraList from '../widgets/SelectedCameraList';
|
|||||||
import SelectedHostList from '../widgets/SelectedHostList';
|
import SelectedHostList from '../widgets/SelectedHostList';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
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',
|
hostId: 'hostId',
|
||||||
cameraId: 'cameraId',
|
cameraId: 'cameraId',
|
||||||
date: 'date',
|
startDay: 'startDay',
|
||||||
|
endDay: 'endDay',
|
||||||
hour: 'hour',
|
hour: 'hour',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,13 +30,16 @@ const RecordingsPage = observer(() => {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryParams = new URLSearchParams(location.search)
|
const queryParams = new URLSearchParams(location.search)
|
||||||
const paramHostId = queryParams.get(recordingsQuery.hostId)
|
const paramHostId = queryParams.get(recordingsPageQuery.hostId)
|
||||||
const paramCameraId = queryParams.get(recordingsQuery.cameraId);
|
const paramCameraId = queryParams.get(recordingsPageQuery.cameraId);
|
||||||
const paramDate = queryParams.get(recordingsQuery.date);
|
const paramStartDay = queryParams.get(recordingsPageQuery.startDay);
|
||||||
const paramTime = queryParams.get(recordingsQuery.hour);
|
const paramEndDay = queryParams.get(recordingsPageQuery.endDay);
|
||||||
|
const paramTime = queryParams.get(recordingsPageQuery.hour);
|
||||||
|
|
||||||
const [hostId, setHostId] = useState<string>('')
|
const [hostId, setHostId] = useState<string>('')
|
||||||
const [cameraId, setCameraId] = useState<string>('')
|
const [cameraId, setCameraId] = useState<string>('')
|
||||||
|
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
|
||||||
|
const [firstRender, setFirstRender] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sideBarsStore.rightVisible = true
|
sideBarsStore.rightVisible = true
|
||||||
@ -39,15 +48,21 @@ const RecordingsPage = observer(() => {
|
|||||||
)
|
)
|
||||||
if (paramHostId) recStore.hostIdParam = paramHostId
|
if (paramHostId) recStore.hostIdParam = paramHostId
|
||||||
if (paramCameraId) recStore.cameraIdParam = paramCameraId
|
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)
|
return () => sideBarsStore.setRightChildren(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHostId(recStore.selectedHost?.id || '')
|
setHostId(recStore.selectedHost?.id || '')
|
||||||
if (recStore.selectedHost) {
|
if (recStore.selectedHost) {
|
||||||
queryParams.set(recordingsQuery.hostId, recStore.selectedHost.id)
|
queryParams.set(recordingsPageQuery.hostId, recStore.selectedHost.id)
|
||||||
} else {
|
} else {
|
||||||
queryParams.delete(recordingsQuery.hostId)
|
queryParams.delete(recordingsPageQuery.hostId)
|
||||||
}
|
}
|
||||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||||
}, [recStore.selectedHost])
|
}, [recStore.selectedHost])
|
||||||
@ -55,26 +70,61 @@ const RecordingsPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCameraId(recStore.selectedCamera?.id || '')
|
setCameraId(recStore.selectedCamera?.id || '')
|
||||||
if (recStore.selectedCamera) {
|
if (recStore.selectedCamera) {
|
||||||
queryParams.set(recordingsQuery.cameraId, recStore.selectedCamera?.id)
|
queryParams.set(recordingsPageQuery.cameraId, recStore.selectedCamera?.id)
|
||||||
} else {
|
} else {
|
||||||
console.log('delete recordingsQuery.cameraId')
|
queryParams.delete(recordingsPageQuery.cameraId)
|
||||||
queryParams.delete(recordingsQuery.cameraId)
|
|
||||||
}
|
}
|
||||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||||
}, [recStore.selectedCamera])
|
}, [recStore.selectedCamera])
|
||||||
|
|
||||||
if (cameraId) {
|
useEffect(() => {
|
||||||
return <SelectedCameraList cameraId={cameraId} />
|
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} />
|
return <SelectedHostList hostId={hostId} />
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('RecordingsPage rendered')
|
|
||||||
return (
|
return (
|
||||||
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
|
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
|
||||||
|
{!hostId ?
|
||||||
<Text size='xl'>Please select host</Text>
|
<Text size='xl'>Please select host</Text>
|
||||||
|
: <></>}
|
||||||
|
{hostId && !(startDay && endDay) ?
|
||||||
|
<Text size='xl'>Please select date</Text>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
|
|
||||||
interface RetryErrorProps {
|
interface RetryErrorPageProps {
|
||||||
onRetry?: () => void
|
onRetry?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const RetryError = ({ onRetry }: RetryErrorProps) => {
|
const RetryErrorPage = ({ onRetry }: RetryErrorPageProps) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { sideBarsStore } = useContext(Context)
|
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'
|
} from '@tanstack/react-query'
|
||||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import { Button, Flex, Space } from '@mantine/core';
|
import { Button, Flex, Space } from '@mantine/core';
|
||||||
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
@ -83,7 +83,7 @@ const SettingsPage = () => {
|
|||||||
|
|
||||||
if (configPending) return <CenterLoader />
|
if (configPending) return <CenterLoader />
|
||||||
|
|
||||||
if (configError) return <RetryError onRetry={refetch} />
|
if (configError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h='100%'>
|
<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 {JSX} from "react";
|
||||||
import Test from "../pages/Test"
|
import Test from "../pages/TestPage"
|
||||||
import MainBody from "../pages/MainBody";
|
import MainPage from "../pages/MainPage";
|
||||||
import {routesPath} from "./routes.path";
|
import {routesPath} from "./routes.path";
|
||||||
import RetryError from "../pages/RetryError";
|
import RetryErrorPage from "../pages/RetryErrorPage";
|
||||||
import Forbidden from "../pages/403";
|
import Forbidden from "../pages/403";
|
||||||
import NotFound from "../pages/404";
|
import NotFound from "../pages/404";
|
||||||
import SettingsPage from "../pages/SettingsPage";
|
import SettingsPage from "../pages/SettingsPage";
|
||||||
@ -53,11 +53,11 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routesPath.MAIN_PATH,
|
path: routesPath.MAIN_PATH,
|
||||||
component: <MainBody />,
|
component: <MainPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routesPath.RETRY_ERROR_PATH,
|
path: routesPath.RETRY_ERROR_PATH,
|
||||||
component: <RetryError />,
|
component: <RetryErrorPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routesPath.FORBIDDEN_ERROR_PATH,
|
path: routesPath.FORBIDDEN_ERROR_PATH,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { CameraConfig } from "../../../types/frigateConfig";
|
import { CameraConfig } from "../../types/frigateConfig";
|
||||||
import { AspectRatio, Flex, createStyles, Text } from "@mantine/core";
|
import { Flex, Text } from "@mantine/core";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import CenterLoader from "../CenterLoader";
|
import CenterLoader from "./CenterLoader";
|
||||||
import axios from "axios";
|
import { frigateApi, proxyApi } from "../../services/frigate.proxy/frigate.api";
|
||||||
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;
|
className?: string;
|
||||||
cameraConfig?: CameraConfig;
|
cameraConfig?: CameraConfig;
|
||||||
onload?: () => void;
|
onload?: () => void;
|
||||||
@ -18,26 +19,29 @@ const AutoUpdatedImage = ({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
enabled,
|
enabled,
|
||||||
...rest
|
...rest
|
||||||
}: CameraImageProps) => {
|
}: AutoUpdatedImageProps) => {
|
||||||
|
const { ref, entry } = useIntersection({threshold: 0.1,})
|
||||||
|
const isVisible = entry?.isIntersecting
|
||||||
|
|
||||||
const { data: imageBlob, refetch, isPending, isError } = useQuery({
|
const { data: imageBlob, refetch, isPending, isError } = useQuery({
|
||||||
queryKey: ['image', imageUrl],
|
queryKey: ['image', imageUrl],
|
||||||
queryFn: () => proxyApi.getImageFrigate(imageUrl),
|
queryFn: () => proxyApi.getImageFrigate(imageUrl),
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
refetchInterval: 60 * 1000,
|
refetchInterval: isVisible ? 30 * 1000 : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
console.log('imageUrl is visible')
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
refetch();
|
refetch();
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [refetch]);
|
}
|
||||||
|
}, [refetch, isVisible]);
|
||||||
|
|
||||||
|
if (isPending) return <CogwheelLoader />
|
||||||
|
|
||||||
if (isPending) return <CenterLoader />
|
|
||||||
|
|
||||||
if (isError) return (
|
if (isError) return (
|
||||||
<Flex direction="column" justify="center" h="100%">
|
<Flex direction="column" justify="center" h="100%">
|
||||||
@ -51,7 +55,7 @@ const AutoUpdatedImage = ({
|
|||||||
|
|
||||||
return (
|
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%">
|
<Flex direction="column" justify="center" h="100%">
|
||||||
<Text align="center">Camera is disabled in config, no stream or snapshot available!</Text>
|
<Text align="center">Camera is disabled in config, no stream or snapshot available!</Text>
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CameraConfig } from '../../types/frigateConfig';
|
import { CameraConfig } from '../../types/frigateConfig';
|
||||||
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { routesPath } from '../../router/routes.path';
|
import { routesPath } from '../../router/routes.path';
|
||||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
|
||||||
import { frigateApi, mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api';
|
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) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
@ -50,7 +50,8 @@ const CameraCard = ({
|
|||||||
navigate(url)
|
navigate(url)
|
||||||
}
|
}
|
||||||
const handleOpenRecordings = () => {
|
const handleOpenRecordings = () => {
|
||||||
throw Error('Not yet implemented')
|
const url = `${routesPath.RECORDINGS_PATH}?${recordingsPageQuery.hostId}=${camera.frigateHost?.id}&${recordingsPageQuery.cameraId}=${camera.id}`
|
||||||
|
navigate(url)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Grid.Col md={6} lg={3} p='0.2rem'>
|
<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 { useMediaQuery } from '@mantine/hooks';
|
||||||
import { dimensions } from '../dimensions/dimensions';
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import KomponentLoader from './CogwheelLoader';
|
import KomponentLoader from './loaders/CogwheelLoader';
|
||||||
import ProductParameter from './ProductParameter';
|
import ProductParameter from './ProductParameter';
|
||||||
import { productString } from '../strings/product.strings';
|
import { productString } from '../strings/product.strings';
|
||||||
import { IconArrowBadgeLeft, IconArrowBadgeRight } from '@tabler/icons-react';
|
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 React, { useContext, useEffect, useState } from 'react';
|
||||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@ -6,20 +6,23 @@ import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services
|
|||||||
import DayAccordion from './DayAccordion';
|
import DayAccordion from './DayAccordion';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Context } from '../../..';
|
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 {
|
interface CameraAccordionProps {
|
||||||
camera: GetCameraWHostWConfig,
|
camera: GetCameraWHostWConfig,
|
||||||
host: GetFrigateHost
|
host: GetFrigateHost
|
||||||
}
|
}
|
||||||
|
|
||||||
const CameraAccordion = observer(({
|
const CameraAccordion = ({
|
||||||
camera,
|
camera,
|
||||||
host
|
host
|
||||||
}: CameraAccordionProps) => {
|
}: CameraAccordionProps) => {
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
const { data, isPending, isError } = useQuery({
|
const { data, isPending, isError, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (camera && host) {
|
if (camera && host) {
|
||||||
@ -44,29 +47,46 @@ const CameraAccordion = observer(({
|
|||||||
setOpenedDay(value)
|
setOpenedDay(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPending) return <Center><Text>Loading...</Text></Center>
|
if (isPending) return <Center><Loader /></Center>
|
||||||
if (isError) return <Center><Text>Loading error</Text></Center>
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
|
|
||||||
if (!data || !camera) return null
|
if (!data || !camera) return null
|
||||||
|
|
||||||
|
const recodItem = (record: RecordSummary) => (
|
||||||
const days = data.slice(0, 2).map(rec => (
|
<Accordion.Item key={record.day} value={record.day}>
|
||||||
<Accordion.Item key={rec.day} value={rec.day}>
|
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control>
|
||||||
<Accordion.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
|
<Accordion.Panel key={record.day + 'panel'}>
|
||||||
<Accordion.Panel key={rec.day + 'panel'}>
|
<DayAccordion key={record.day + 'day'} recordSummary={record} />
|
||||||
<DayAccordion key={rec.day + 'day'} recordSummary={rec} />
|
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</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')
|
console.log('CameraAccordion rendered')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
||||||
{days}
|
{days()}
|
||||||
</Accordion>
|
</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 React, { useContext, useEffect, useState } from 'react';
|
||||||
import { RecordHour, RecordSummary, Recording } from '../../../types/record';
|
import { RecordSummary } from '../../../types/record';
|
||||||
import Button from '../frigate/Button';
|
|
||||||
import { IconPlayerPause, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import PlayControl from './PlayControl';
|
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 { Context } from '../../..';
|
||||||
import VideoPlayer from '../frigate/VideoPlayer';
|
import VideoPlayer from '../players/VideoPlayer';
|
||||||
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
import { getResolvedTimeZone } from '../../utils/dateUtil';
|
||||||
import EventsAccordion from './EventsAccordion';
|
|
||||||
import DayEventsAccordion from './DayEventsAccordion';
|
import DayEventsAccordion from './DayEventsAccordion';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
|
||||||
interface RecordingAccordionProps {
|
interface RecordingAccordionProps {
|
||||||
recordSummary?: RecordSummary
|
recordSummary?: RecordSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
const DayAccordion = observer(({
|
const DayAccordion = ({
|
||||||
recordSummary
|
recordSummary
|
||||||
}: RecordingAccordionProps) => {
|
}: RecordingAccordionProps) => {
|
||||||
const { recordingsStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
||||||
const [openedValue, setOpenedValue] = useState<string>()
|
const [openedValue, setOpenedValue] = useState<string>()
|
||||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||||
@ -28,11 +26,11 @@ const DayAccordion = observer(({
|
|||||||
if (openVideoPlayer) {
|
if (openVideoPlayer) {
|
||||||
console.log('openVideoPlayer', openVideoPlayer)
|
console.log('openVideoPlayer', openVideoPlayer)
|
||||||
if (openVideoPlayer) {
|
if (openVideoPlayer) {
|
||||||
recordingsStore.recordToPlay.day = recordSummary?.day
|
recStore.recordToPlay.day = recordSummary?.day
|
||||||
recordingsStore.recordToPlay.hour = openVideoPlayer
|
recStore.recordToPlay.hour = openVideoPlayer
|
||||||
recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
recStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||||
const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay)
|
const parsed = recStore.getFullRecordForPlay(recStore.recordToPlay)
|
||||||
console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay)
|
console.log('recordingsStore.playedRecord: ', recStore.recordToPlay)
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
const url = proxyApi.recordingURL(
|
const url = proxyApi.recordingURL(
|
||||||
parsed.data.hostName,
|
parsed.data.hostName,
|
||||||
@ -50,10 +48,10 @@ const DayAccordion = observer(({
|
|||||||
}
|
}
|
||||||
}, [openVideoPlayer])
|
}, [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) => {
|
const handleOpenPlayer = (hour: string) => {
|
||||||
// console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`)
|
|
||||||
if (openVideoPlayer !== hour) {
|
if (openVideoPlayer !== hour) {
|
||||||
setOpenedValue(hour)
|
setOpenedValue(hour)
|
||||||
setOpenVideoPlayer(hour)
|
setOpenVideoPlayer(hour)
|
||||||
@ -73,6 +71,17 @@ const DayAccordion = observer(({
|
|||||||
|
|
||||||
console.log('DayAccordion rendered')
|
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 (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
key={recordSummary.day}
|
key={recordSummary.day}
|
||||||
@ -81,23 +90,27 @@ const DayAccordion = observer(({
|
|||||||
value={openedValue}
|
value={openedValue}
|
||||||
onChange={handleClick}
|
onChange={handleClick}
|
||||||
>
|
>
|
||||||
{recordSummary.hours.slice(0, 5).map(hour => (
|
{recordSummary.hours.map(hour => (
|
||||||
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
||||||
<Accordion.Control key={hour.hour + 'Control'}>
|
<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.Control>
|
||||||
<Accordion.Panel key={hour.hour + 'Panel'}>
|
<Accordion.Panel key={hour.hour + 'Panel'}>
|
||||||
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||||
{hour.events > 0 ?
|
{hour.events > 0 ?
|
||||||
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
|
<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.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
export default DayAccordion;
|
export default observer(DayAccordion);
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Accordion, Text } from '@mantine/core';
|
import { Accordion, Text } from '@mantine/core';
|
||||||
import React, { Suspense, lazy, useState } from 'react';
|
import React, { Suspense, lazy, useState } from 'react';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
const EventsAccordion = lazy(() => import('./EventsAccordion'))
|
const EventsAccordion = lazy(() => import('./EventsAccordion'))
|
||||||
|
|
||||||
interface DayEventsAccordionProps {
|
interface DayEventsAccordionProps {
|
||||||
@ -21,7 +22,7 @@ const DayEventsAccordion = ({
|
|||||||
return (
|
return (
|
||||||
<Accordion onChange={handleClick}>
|
<Accordion onChange={handleClick}>
|
||||||
<Accordion.Item value={hour}>
|
<Accordion.Item value={hour}>
|
||||||
<Accordion.Control><Text>Events {qty}</Text></Accordion.Control>
|
<Accordion.Control><Text>{strings.events}: {qty}</Text></Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
{openedItem === hour ?
|
{openedItem === hour ?
|
||||||
<Suspense>
|
<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 { observer } from 'mobx-react-lite';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Context } from '../../..';
|
import { Context } from '../../..';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
||||||
import PlayControl from './PlayControl';
|
import PlayControl from './PlayControl';
|
||||||
import VideoPlayer from '../frigate/VideoPlayer';
|
import VideoPlayer from '../players/VideoPlayer';
|
||||||
import { formatUnixTimestampToDateTime, getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../frigate/dateUtil';
|
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
|
* @param day frigate format, e.g day: 2024-02-23
|
||||||
@ -31,8 +34,6 @@ interface EventsAccordionProps {
|
|||||||
const EventsAccordion = observer(({
|
const EventsAccordion = observer(({
|
||||||
day,
|
day,
|
||||||
hour,
|
hour,
|
||||||
cameraName,
|
|
||||||
hostName,
|
|
||||||
// TODO labels, score
|
// TODO labels, score
|
||||||
}: EventsAccordionProps) => {
|
}: EventsAccordionProps) => {
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
@ -40,17 +41,17 @@ const EventsAccordion = observer(({
|
|||||||
const [openedValue, setOpenedValue] = useState<string>()
|
const [openedValue, setOpenedValue] = useState<string>()
|
||||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||||
|
|
||||||
const inHostName = hostName || recStore.recordToPlay.hostName
|
const inHost = recStore.selectedHost
|
||||||
const inCameraName = cameraName || recStore.recordToPlay.cameraName
|
const inCamera = recStore.selectedCamera
|
||||||
const isRequiredParams = inCameraName && inHostName
|
const isRequiredParams = inHost && inCamera
|
||||||
const { data, isPending, isError, refetch } = useQuery({
|
const { data, isPending, isError, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getEvents, day, hour, inCameraName, inHostName],
|
queryKey: [frigateQueryKeys.getEvents, inHost, inCamera, day, hour],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!isRequiredParams) return null
|
if (!isRequiredParams) return null
|
||||||
const [startTime, endTime] = getUnixTime(day, hour)
|
const [startTime, endTime] = getUnixTime(day, hour)
|
||||||
const parsed = getEventsQuerySchema.safeParse({
|
const parsed = getEventsQuerySchema.safeParse({
|
||||||
hostName: inHostName,
|
hostName: mapHostToHostname(inHost),
|
||||||
camerasName: [inCameraName],
|
camerasName: [inCamera.name],
|
||||||
after: startTime,
|
after: startTime,
|
||||||
before: endTime,
|
before: endTime,
|
||||||
hasClip: true,
|
hasClip: true,
|
||||||
@ -78,8 +79,8 @@ const EventsAccordion = observer(({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openVideoPlayer) {
|
if (openVideoPlayer) {
|
||||||
console.log('openVideoPlayer', openVideoPlayer)
|
console.log('openVideoPlayer', openVideoPlayer)
|
||||||
if (openVideoPlayer && inHostName) {
|
if (openVideoPlayer && inHost) {
|
||||||
const url = proxyApi.eventURL(inHostName, openVideoPlayer)
|
const url = proxyApi.eventURL(mapHostToHostname(inHost), openVideoPlayer)
|
||||||
console.log('GET EVENT URL: ', url)
|
console.log('GET EVENT URL: ', url)
|
||||||
setPlayerUrl(url)
|
setPlayerUrl(url)
|
||||||
}
|
}
|
||||||
@ -88,8 +89,8 @@ const EventsAccordion = observer(({
|
|||||||
}
|
}
|
||||||
}, [openVideoPlayer])
|
}, [openVideoPlayer])
|
||||||
|
|
||||||
if (isPending) return <Center><Text>Loading...</Text></Center>
|
if (isPending) return <Center><Loader /></Center>
|
||||||
if (isError) return <Center><Text>Loading error</Text></Center>
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
||||||
|
|
||||||
const handleOpenPlayer = (eventId: string) => {
|
const handleOpenPlayer = (eventId: string) => {
|
||||||
@ -111,6 +112,20 @@ const EventsAccordion = observer(({
|
|||||||
setOpenVideoPlayer(undefined)
|
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 (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
variant='separated'
|
variant='separated'
|
||||||
@ -118,21 +133,25 @@ const EventsAccordion = observer(({
|
|||||||
value={openedValue}
|
value={openedValue}
|
||||||
onChange={handleClick}
|
onChange={handleClick}
|
||||||
>
|
>
|
||||||
{data.slice(0, 5).map(event => (
|
{data.map(event => (
|
||||||
<Accordion.Item key={event.id + 'Item'} value={event.id}>
|
<Accordion.Item key={event.id + 'Item'} value={event.id}>
|
||||||
<Accordion.Control key={event.id + 'Control'}>
|
<Accordion.Control key={event.id + 'Control'}>
|
||||||
<PlayControl
|
<PlayControl
|
||||||
label={unixTimeToDate(event.start_time)}
|
label={eventLabel(event)}
|
||||||
value={event.id}
|
value={event.id}
|
||||||
openVideoPlayer={openVideoPlayer}
|
openVideoPlayer={openVideoPlayer}
|
||||||
onClick={handleOpenPlayer} />
|
onClick={handleOpenPlayer} />
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel key={event.id + 'Panel'}>
|
<Accordion.Panel key={event.id + 'Panel'}>
|
||||||
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||||
<Text>Camera: {event.camera}</Text>
|
<Group mt='1rem'>
|
||||||
<Text>Label: {event.label}</Text>
|
<Text>{strings.camera}: {event.camera}</Text>
|
||||||
<Text>Start: {unixTimeToDate(event.start_time)}</Text>
|
<Text>{strings.player.object}: {event.label}</Text>
|
||||||
<Text>Duration: {getDurationFromTimestamps(event.start_time, event.end_time)}</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.Panel>
|
||||||
</Accordion.Item>
|
</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 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 {
|
interface PlayControlProps {
|
||||||
label: string,
|
label: string | JSX.Element,
|
||||||
value: string,
|
value: string,
|
||||||
openVideoPlayer?: string,
|
openVideoPlayer?: string,
|
||||||
onClick?: (value: string) => void
|
onClick?: (value: string) => void
|
||||||
@ -15,26 +39,38 @@ const PlayControl = ({
|
|||||||
openVideoPlayer,
|
openVideoPlayer,
|
||||||
onClick
|
onClick
|
||||||
}: PlayControlProps) => {
|
}: PlayControlProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
const handleClick = (value: string) => {
|
const handleClick = (value: string) => {
|
||||||
if (onClick) onClick(value)
|
if (onClick) onClick(value)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Flex justify='space-between'>
|
<Flex justify='space-between'>
|
||||||
{label}
|
{label}
|
||||||
<Group>
|
<Group className={classes.group}
|
||||||
<Text onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
handleClick(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className={classes.text}
|
||||||
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(value)
|
handleClick(value)
|
||||||
}}>
|
}}>
|
||||||
{openVideoPlayer === value ? 'Stop Video' : 'Play Video'}
|
{openVideoPlayer === value ? strings.player.stopVideo : strings.player.startVideo}
|
||||||
</Text>
|
</Text>
|
||||||
{openVideoPlayer === value ?
|
{openVideoPlayer === value ?
|
||||||
<IconPlayerStop onClick={(event) => {
|
<IconPlayerStopFilled
|
||||||
|
className={classes.iconStop}
|
||||||
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(value)
|
handleClick(value)
|
||||||
}} />
|
}} />
|
||||||
:
|
:
|
||||||
<IconPlayerPlay onClick={(event) => {
|
<IconPlayerPlayFilled
|
||||||
|
className={classes.iconPlay}
|
||||||
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(value)
|
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 { Context } from '../../..';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||||
import CogwheelLoader from '../CogwheelLoader';
|
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||||
import { Center, Text } from '@mantine/core';
|
import { Center, Loader, Text } from '@mantine/core';
|
||||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
import RetryError from '../RetryError';
|
||||||
|
|
||||||
interface CameraSelectFilterProps {
|
interface CameraSelectFilterProps {
|
||||||
selectedHostId: string,
|
selectedHostId: string,
|
||||||
@ -16,7 +18,7 @@ const CameraSelectFilter = ({
|
|||||||
}: CameraSelectFilterProps) => {
|
}: CameraSelectFilterProps) => {
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
const { data, isError, isPending, isSuccess } = useQuery({
|
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId],
|
queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId],
|
||||||
queryFn: () => frigateApi.getHost(selectedHostId)
|
queryFn: () => frigateApi.getHost(selectedHostId)
|
||||||
})
|
})
|
||||||
@ -30,8 +32,8 @@ const CameraSelectFilter = ({
|
|||||||
}
|
}
|
||||||
}, [isSuccess])
|
}, [isSuccess])
|
||||||
|
|
||||||
if (isPending) return <CogwheelLoader />
|
if (isPending) return <Loader />
|
||||||
if (isError) return <Center><Text>Loading error!</Text></Center>
|
if (isError) return <RetryError onRetry={refetch}/>
|
||||||
if (!data) return null
|
if (!data) return null
|
||||||
|
|
||||||
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
|
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
|
||||||
@ -51,7 +53,7 @@ const CameraSelectFilter = ({
|
|||||||
return (
|
return (
|
||||||
<OneSelectFilter
|
<OneSelectFilter
|
||||||
id='frigate-cameras'
|
id='frigate-cameras'
|
||||||
label='Select camera:'
|
label={strings.selectCamera}
|
||||||
spaceBetween='1rem'
|
spaceBetween='1rem'
|
||||||
value={recStore.selectedCamera?.id || ''}
|
value={recStore.selectedCamera?.id || ''}
|
||||||
defaultValue={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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import CameraImage from "./CameraImage";
|
// import CameraImage from "./CameraImage";
|
||||||
import { CameraConfig } from "../../../types/frigateConfig";
|
// import { CameraConfig } from "../../../types/frigateConfig";
|
||||||
import { useDocumentVisibility } from "@mantine/hooks";
|
// import { useDocumentVisibility } from "@mantine/hooks";
|
||||||
import { AspectRatio, Flex } from "@mantine/core";
|
// import { AspectRatio, Flex } from "@mantine/core";
|
||||||
|
|
||||||
interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
export {}
|
||||||
cameraConfig?: CameraConfig
|
|
||||||
searchParams?: {};
|
|
||||||
showFps?: boolean;
|
|
||||||
className?: string;
|
|
||||||
url: string
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO Delete
|
// interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
export default function AutoUpdatingCameraImage({
|
// cameraConfig?: CameraConfig
|
||||||
cameraConfig,
|
// searchParams?: {};
|
||||||
searchParams = "",
|
// showFps?: boolean;
|
||||||
showFps = true,
|
// className?: string;
|
||||||
className,
|
// url: string
|
||||||
url,
|
// };
|
||||||
...rest
|
|
||||||
}: AutoUpdatingCameraImageProps) {
|
|
||||||
const [key, setKey] = useState(Date.now());
|
|
||||||
const [fps, setFps] = useState<string>("0");
|
|
||||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
|
||||||
|
|
||||||
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(() => {
|
// const reloadInterval = useMemo(() => {
|
||||||
if (windowVisible === "hidden") {
|
// if (windowVisible === "hidden") {
|
||||||
return -1; // no reason to update the image when the window is not visible
|
// return -1; // no reason to update the image when the window is not visible
|
||||||
}
|
|
||||||
|
|
||||||
// if (liveReady) {
|
|
||||||
// return 60000;
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (cameraActive) {
|
// // if (liveReady) {
|
||||||
// return 200;
|
// // return 60000;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // if (cameraActive) {
|
||||||
|
// // return 200;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// return 30000;
|
||||||
|
// }, [windowVisible]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (reloadInterval == -1) {
|
||||||
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return 30000;
|
// setKey(Date.now());
|
||||||
}, [windowVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// return () => {
|
||||||
if (reloadInterval == -1) {
|
// if (timeoutId) {
|
||||||
return;
|
// clearTimeout(timeoutId);
|
||||||
}
|
// setTimeoutId(undefined);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }, [reloadInterval]);
|
||||||
|
|
||||||
setKey(Date.now());
|
// const handleLoad = useCallback(() => {
|
||||||
|
// if (reloadInterval == -1) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
return () => {
|
// const loadTime = Date.now() - key;
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
setTimeoutId(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [reloadInterval]);
|
|
||||||
|
|
||||||
const handleLoad = useCallback(() => {
|
// if (showFps) {
|
||||||
if (reloadInterval == -1) {
|
// setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
|
||||||
return;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
const loadTime = Date.now() - key;
|
// setTimeoutId(
|
||||||
|
// setTimeout(
|
||||||
|
// () => {
|
||||||
|
// setKey(Date.now());
|
||||||
|
// },
|
||||||
|
// loadTime > reloadInterval ? 1 : reloadInterval
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// }, [key, setFps]);
|
||||||
|
|
||||||
if (showFps) {
|
// return (
|
||||||
setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
|
// // <AspectRatio ratio={1}>
|
||||||
}
|
// <Flex direction='column' h='100%'>
|
||||||
|
// {/* <CameraImage
|
||||||
setTimeoutId(
|
// cameraConfig={cameraConfig}
|
||||||
setTimeout(
|
// onload={handleLoad}
|
||||||
() => {
|
// enabled={cameraConfig?.enabled}
|
||||||
setKey(Date.now());
|
// url={url}
|
||||||
},
|
// {...rest}
|
||||||
loadTime > reloadInterval ? 1 : reloadInterval
|
// /> */}
|
||||||
)
|
// {showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||||
);
|
// </Flex>
|
||||||
}, [key, setFps]);
|
// // </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 Tooltip from './Tooltip';
|
||||||
import { Fragment, useCallback, useRef, useState } from 'react';
|
// import { Fragment, useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
const ButtonColors = {
|
export {}
|
||||||
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: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ButtonTypes = {
|
// const ButtonColors = {
|
||||||
contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
|
// blue: {
|
||||||
outlined: '',
|
// contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300',
|
||||||
text: 'transition-opacity',
|
// outlined:
|
||||||
iconOnly: 'transition-opacity',
|
// '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({
|
// const ButtonTypes = {
|
||||||
children,
|
// contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
|
||||||
className = '',
|
// outlined: '',
|
||||||
color = 'blue',
|
// text: 'transition-opacity',
|
||||||
disabled = false,
|
// iconOnly: 'transition-opacity',
|
||||||
ariaCapitalize = false,
|
// };
|
||||||
href,
|
|
||||||
target,
|
|
||||||
type = 'contained',
|
|
||||||
...attrs
|
|
||||||
}) {
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
const ref = useRef();
|
|
||||||
|
|
||||||
let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
|
// export default function Button({
|
||||||
ButtonColors[disabled ? 'disabled' : color][type]
|
// children,
|
||||||
} 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 ${
|
// className = '',
|
||||||
disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer`
|
// color = 'blue',
|
||||||
}`;
|
// disabled = false,
|
||||||
|
// ariaCapitalize = false,
|
||||||
|
// href,
|
||||||
|
// target,
|
||||||
|
// type = 'contained',
|
||||||
|
// ...attrs
|
||||||
|
// }) {
|
||||||
|
// const [hovered, setHovered] = useState(false);
|
||||||
|
// const ref = useRef();
|
||||||
|
|
||||||
if (disabled) {
|
// let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
|
||||||
classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
|
// 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(() => {
|
// if (disabled) {
|
||||||
setHovered(true);
|
// classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
|
||||||
}, []);
|
// }
|
||||||
|
|
||||||
const handleMouseleave = useCallback(() => {
|
// const handleMousenter = useCallback(() => {
|
||||||
setHovered(false);
|
// setHovered(true);
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
const Element = href ? 'a' : 'div';
|
// const handleMouseleave = useCallback(() => {
|
||||||
|
// setHovered(false);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
return (
|
// const Element = href ? 'a' : 'div';
|
||||||
<Fragment>
|
|
||||||
<Element
|
// return (
|
||||||
role="button"
|
// <Fragment>
|
||||||
aria-disabled={disabled ? 'true' : 'false'}
|
// <Element
|
||||||
tabindex="0"
|
// role="button"
|
||||||
className={classes}
|
// aria-disabled={disabled ? 'true' : 'false'}
|
||||||
href={href}
|
// tabindex="0"
|
||||||
target={target}
|
// className={classes}
|
||||||
ref={ref}
|
// href={href}
|
||||||
onmouseenter={handleMousenter}
|
// target={target}
|
||||||
onmouseleave={handleMouseleave}
|
// ref={ref}
|
||||||
{...attrs}
|
// onmouseenter={handleMousenter}
|
||||||
>
|
// onmouseleave={handleMouseleave}
|
||||||
{children}
|
// {...attrs}
|
||||||
</Element>
|
// >
|
||||||
{hovered && attrs['aria-label'] ? <Tooltip text={attrs['aria-label']} relativeTo={ref} capitalize={ariaCapitalize} /> : null}
|
// {children}
|
||||||
</Fragment>
|
// </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 { useCallback, useMemo, useState } from "react";
|
||||||
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
|
// import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
|
||||||
import { CameraConfig } from "../../../types/frigateConfig";
|
// import { CameraConfig } from "../../../types/frigateConfig";
|
||||||
import { usePersistence } from "../../../hooks/use-persistence";
|
// import { usePersistence } from "../../../hooks/use-persistence";
|
||||||
import { Button, Switch, Text } from "@mantine/core";
|
// import { Button, Switch, Text } from "@mantine/core";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
// import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
||||||
import { IconSettings } from "@tabler/icons-react";
|
// import { IconSettings } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
|
||||||
type Options = { [key: string]: boolean };
|
export {}
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
// type Options = { [key: string]: boolean };
|
||||||
|
|
||||||
type DebugCameraImageProps = {
|
// const emptyObject = Object.freeze({});
|
||||||
className?: string;
|
|
||||||
cameraConfig: CameraConfig
|
|
||||||
url: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DebugCameraImage({
|
// type DebugCameraImageProps = {
|
||||||
className,
|
// className?: string;
|
||||||
cameraConfig,
|
// cameraConfig: CameraConfig
|
||||||
url,
|
// url: string
|
||||||
}: 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]);
|
|
||||||
|
|
||||||
return (
|
// export default function DebugCameraImage({
|
||||||
<div className={className}>
|
// className,
|
||||||
<AutoUpdatingCameraImage
|
// cameraConfig,
|
||||||
cameraConfig={cameraConfig}
|
// url,
|
||||||
searchParams={searchParams}
|
// }: DebugCameraImageProps) {
|
||||||
url={url}
|
// const [showSettings, setShowSettings] = useState(false);
|
||||||
/>
|
// const [options, setOptions] = usePersistence(
|
||||||
<Button onClick={handleToggleSettings} variant="link" size="sm">
|
// `${cameraConfig?.name}-feed`,
|
||||||
<span className="w-5 h-5">
|
// emptyObject
|
||||||
<IconSettings />
|
// );
|
||||||
</span>{" "}
|
// const handleSetOption = useCallback(
|
||||||
<span>{showSettings ? "Hide" : "Show"} Options</span>
|
// (id: string, value: boolean) => {
|
||||||
</Button>
|
// const newOptions = { ...options, [id]: value };
|
||||||
{showSettings ? (
|
// setOptions(newOptions);
|
||||||
<Card>
|
// },
|
||||||
<CardHeader>
|
// [options]
|
||||||
<CardTitle>Options</CardTitle>
|
// );
|
||||||
</CardHeader>
|
// const searchParams = useMemo(
|
||||||
<CardContent>
|
// () =>
|
||||||
<DebugSettings
|
// new URLSearchParams(
|
||||||
handleSetOption={handleSetOption}
|
// Object.keys(options).reduce((memo, key) => {
|
||||||
options={options}
|
// //@ts-ignore we know this is correct
|
||||||
/>
|
// memo.push([key, options[key] === true ? "1" : "0"]);
|
||||||
</CardContent>
|
// return memo;
|
||||||
</Card>
|
// }, [])
|
||||||
) : null}
|
// ),
|
||||||
</div>
|
// [options]
|
||||||
);
|
// );
|
||||||
}
|
// const handleToggleSettings = useCallback(() => {
|
||||||
|
// setShowSettings(!showSettings);
|
||||||
|
// }, [showSettings]);
|
||||||
|
|
||||||
type DebugSettingsProps = {
|
// return (
|
||||||
handleSetOption: (id: string, value: boolean) => void;
|
// <div className={className}>
|
||||||
options: Options;
|
// <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) {
|
// type DebugSettingsProps = {
|
||||||
return (
|
// handleSetOption: (id: string, value: boolean) => void;
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
// options: Options;
|
||||||
<div className="flex items-center space-x-2">
|
// };
|
||||||
<Switch
|
|
||||||
id="bbox"
|
// function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||||
checked={options["bbox"]}
|
// return (
|
||||||
onChange={() => { }}
|
// <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
// onCheckedChange={(isChecked) => {
|
// <div className="flex items-center space-x-2">
|
||||||
// handleSetOption("bbox", isChecked);
|
// <Switch
|
||||||
// }}
|
// id="bbox"
|
||||||
/>
|
// checked={options["bbox"]}
|
||||||
{/* <Label htmlFor="bbox">Bounding Box</Label> */}
|
// onChange={() => { }}
|
||||||
<Text>Bounding Box</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("bbox", isChecked);
|
||||||
<div className="flex items-center space-x-2">
|
// // }}
|
||||||
<Switch
|
// />
|
||||||
id="timestamp"
|
// {/* <Label htmlFor="bbox">Bounding Box</Label> */}
|
||||||
checked={options["timestamp"]}
|
// <Text>Bounding Box</Text>
|
||||||
// onCheckedChange={(isChecked) => {
|
// </div>
|
||||||
// handleSetOption("timestamp", isChecked);
|
// <div className="flex items-center space-x-2">
|
||||||
// }}
|
// <Switch
|
||||||
/>
|
// id="timestamp"
|
||||||
{/* <Label htmlFor="timestamp">Timestamp</Label> */}
|
// checked={options["timestamp"]}
|
||||||
<Text>Timestamp</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("timestamp", isChecked);
|
||||||
<div className="flex items-center space-x-2">
|
// // }}
|
||||||
<Switch
|
// />
|
||||||
id="zones"
|
// {/* <Label htmlFor="timestamp">Timestamp</Label> */}
|
||||||
checked={options["zones"]}
|
// <Text>Timestamp</Text>
|
||||||
// onCheckedChange={(isChecked) => {
|
// </div>
|
||||||
// handleSetOption("zones", isChecked);
|
// <div className="flex items-center space-x-2">
|
||||||
// }}
|
// <Switch
|
||||||
/>
|
// id="zones"
|
||||||
{/* <Label htmlFor="zones">Zones</Label> */}
|
// checked={options["zones"]}
|
||||||
<Text>Zones</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("zones", isChecked);
|
||||||
<div className="flex items-center space-x-2">
|
// // }}
|
||||||
<Switch
|
// />
|
||||||
id="mask"
|
// {/* <Label htmlFor="zones">Zones</Label> */}
|
||||||
checked={options["mask"]}
|
// <Text>Zones</Text>
|
||||||
// onCheckedChange={(isChecked) => {
|
// </div>
|
||||||
// handleSetOption("mask", isChecked);
|
// <div className="flex items-center space-x-2">
|
||||||
// }}
|
// <Switch
|
||||||
/>
|
// id="mask"
|
||||||
{/* <Label htmlFor="mask">Mask</Label> */}
|
// checked={options["mask"]}
|
||||||
<Text>Mask</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("mask", isChecked);
|
||||||
<div className="flex items-center space-x-2">
|
// // }}
|
||||||
<Switch
|
// />
|
||||||
id="motion"
|
// {/* <Label htmlFor="mask">Mask</Label> */}
|
||||||
checked={options["motion"]}
|
// <Text>Mask</Text>
|
||||||
// onCheckedChange={(isChecked) => {
|
// </div>
|
||||||
// handleSetOption("motion", isChecked);
|
// <div className="flex items-center space-x-2">
|
||||||
// }}
|
// <Switch
|
||||||
/>
|
// id="motion"
|
||||||
{/* <Label htmlFor="motion">Motion</Label> */}
|
// checked={options["motion"]}
|
||||||
<Text>Motion</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("motion", isChecked);
|
||||||
<div className="flex items-center space-x-2">
|
// // }}
|
||||||
<Switch
|
// />
|
||||||
id="regions"
|
// {/* <Label htmlFor="motion">Motion</Label> */}
|
||||||
checked={options["regions"]}
|
// <Text>Motion</Text>
|
||||||
// onCheckedChange={(isChecked) => {
|
// </div>
|
||||||
// handleSetOption("regions", isChecked);
|
// <div className="flex items-center space-x-2">
|
||||||
// }}
|
// <Switch
|
||||||
/>
|
// id="regions"
|
||||||
{/* <Label htmlFor="regions">Regions</Label> */}
|
// checked={options["regions"]}
|
||||||
<Text>Regions</Text>
|
// // onCheckedChange={(isChecked) => {
|
||||||
</div>
|
// // handleSetOption("regions", isChecked);
|
||||||
</div>
|
// // }}
|
||||||
);
|
// />
|
||||||
}
|
// {/* <Label htmlFor="regions">Regions</Label> */}
|
||||||
|
// <Text>Regions</Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|||||||
@ -1,35 +1,37 @@
|
|||||||
import { h, Fragment } from 'preact';
|
// import { h, Fragment } from 'preact';
|
||||||
import { createPortal } from 'preact/compat';
|
// import { createPortal } from 'preact/compat';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
// import { useState, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Dialog({ children, portalRootID = 'dialogs' }) {
|
export {}
|
||||||
const portalRoot = portalRootID && document.getElementById(portalRootID);
|
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// export default function Dialog({ children, portalRootID = 'dialogs' }) {
|
||||||
window.requestAnimationFrame(() => {
|
// const portalRoot = portalRootID && document.getElementById(portalRootID);
|
||||||
setShow(true);
|
// const [show, setShow] = useState(false);
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const dialog = (
|
// useEffect(() => {
|
||||||
<Fragment>
|
// window.requestAnimationFrame(() => {
|
||||||
<div
|
// setShow(true);
|
||||||
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;
|
// 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 { h } from 'preact';
|
||||||
import { Link as RouterLink } from 'preact-router/match';
|
// import { Link as RouterLink } from 'preact-router/match';
|
||||||
|
|
||||||
export default function Link({
|
export {}
|
||||||
activeClassName = '',
|
|
||||||
className = 'text-blue-500 hover:underline',
|
// export default function Link({
|
||||||
children,
|
// activeClassName = '',
|
||||||
href,
|
// className = 'text-blue-500 hover:underline',
|
||||||
...props
|
// children,
|
||||||
}) {
|
// href,
|
||||||
return (
|
// ...props
|
||||||
<RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
|
// }) {
|
||||||
{children}
|
// return (
|
||||||
</RouterLink>
|
// <RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
|
||||||
);
|
// {children}
|
||||||
}
|
// </RouterLink>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|||||||
@ -1,48 +1,50 @@
|
|||||||
import { h } from 'preact';
|
// import { h } from 'preact';
|
||||||
import RelativeModal from './RelativeModal';
|
// import RelativeModal from './RelativeModal';
|
||||||
import { useCallback } from 'preact/hooks';
|
// import { useCallback } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
|
export {}
|
||||||
return relativeTo ? (
|
|
||||||
<RelativeModal
|
|
||||||
children={children}
|
|
||||||
className={`${className || ''} py-2`}
|
|
||||||
role="listbox"
|
|
||||||
onDismiss={onDismiss}
|
|
||||||
portalRootID="menus"
|
|
||||||
relativeTo={relativeTo}
|
|
||||||
widthRelative={widthRelative}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
|
// export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
|
||||||
const handleClick = useCallback(() => {
|
// return relativeTo ? (
|
||||||
onSelect && onSelect(value, label);
|
// <RelativeModal
|
||||||
}, [onSelect, value, label]);
|
// 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 (
|
// const Element = href ? 'a' : 'div';
|
||||||
<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 (
|
||||||
return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
|
// <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 { h } from 'preact';
|
||||||
import { useRef, useState } from 'preact/hooks';
|
// import { useRef, useState } from 'preact/hooks';
|
||||||
import Menu from './Menu';
|
// import Menu from './Menu';
|
||||||
import { ArrowDropdown } from '../icons/ArrowDropdown';
|
// import { ArrowDropdown } from '../icons/ArrowDropdown';
|
||||||
import Heading from './Heading';
|
// import Heading from './Heading';
|
||||||
import Button from './Button';
|
// import Button from './Button';
|
||||||
import SelectOnlyIcon from '../icons/SelectOnly';
|
// import SelectOnlyIcon from '../icons/SelectOnly';
|
||||||
|
|
||||||
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
export {}
|
||||||
const popupRef = useRef(null);
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
// export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
||||||
showMenu: false,
|
// const popupRef = useRef(null);
|
||||||
});
|
|
||||||
|
|
||||||
const isOptionSelected = (item) => {
|
// const [state, setState] = useState({
|
||||||
return selection == 'all' || selection.split(',').indexOf(item) > -1;
|
// showMenu: false,
|
||||||
};
|
// });
|
||||||
|
|
||||||
const menuHeight = Math.round(window.innerHeight * 0.55);
|
// const isOptionSelected = (item) => {
|
||||||
return (
|
// return selection == 'all' || selection.split(',').indexOf(item) > -1;
|
||||||
<div className={`${className} p-2`} ref={popupRef}>
|
// };
|
||||||
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
|
||||||
<label>{title}</label>
|
// const menuHeight = Math.round(window.innerHeight * 0.55);
|
||||||
<ArrowDropdown className="w-6" />
|
// return (
|
||||||
</div>
|
// <div className={`${className} p-2`} ref={popupRef}>
|
||||||
{state.showMenu ? (
|
// <div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
||||||
<Menu
|
// <label>{title}</label>
|
||||||
className={`max-h-[${menuHeight}px] overflow-auto`}
|
// <ArrowDropdown className="w-6" />
|
||||||
relativeTo={popupRef}
|
// </div>
|
||||||
onDismiss={() => setState({ showMenu: false })}
|
// {state.showMenu ? (
|
||||||
>
|
// <Menu
|
||||||
<div className="flex flex-wrap justify-between items-center">
|
// className={`max-h-[${menuHeight}px] overflow-auto`}
|
||||||
<Heading className="p-4 justify-center" size="md">
|
// relativeTo={popupRef}
|
||||||
{title}
|
// onDismiss={() => setState({ showMenu: false })}
|
||||||
</Heading>
|
// >
|
||||||
<Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
|
// <div className="flex flex-wrap justify-between items-center">
|
||||||
Show All
|
// <Heading className="p-4 justify-center" size="md">
|
||||||
</Button>
|
// {title}
|
||||||
</div>
|
// </Heading>
|
||||||
{options.map((item) => (
|
// <Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
|
||||||
<div className="flex flex-grow" key={item}>
|
// Show All
|
||||||
<label
|
// </Button>
|
||||||
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`}
|
// </div>
|
||||||
>
|
// {options.map((item) => (
|
||||||
<input
|
// <div className="flex flex-grow" key={item}>
|
||||||
className="mx-4 m-0 align-middle"
|
// <label
|
||||||
type="checkbox"
|
// 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`}
|
||||||
checked={isOptionSelected(item)}
|
// >
|
||||||
onChange={() => onToggle(item)}
|
// <input
|
||||||
/>
|
// className="mx-4 m-0 align-middle"
|
||||||
{item.replaceAll('_', ' ')}
|
// type="checkbox"
|
||||||
</label>
|
// checked={isOptionSelected(item)}
|
||||||
<div className="justify-right">
|
// onChange={() => onToggle(item)}
|
||||||
<Button
|
// />
|
||||||
color={isOptionSelected(item) ? 'blue' : 'black'}
|
// {item.replaceAll('_', ' ')}
|
||||||
type="text"
|
// </label>
|
||||||
className="max-h-[35px] mx-2"
|
// <div className="justify-right">
|
||||||
onClick={() => onSelectSingle(item)}
|
// <Button
|
||||||
>
|
// color={isOptionSelected(item) ? 'blue' : 'black'}
|
||||||
{ ( <SelectOnlyIcon /> ) }
|
// type="text"
|
||||||
</Button>
|
// className="max-h-[35px] mx-2"
|
||||||
</div>
|
// onClick={() => onSelectSingle(item)}
|
||||||
</div>
|
// >
|
||||||
))}
|
// { ( <SelectOnlyIcon /> ) }
|
||||||
</Menu>
|
// </Button>
|
||||||
) : null}
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// ))}
|
||||||
}
|
// </Menu>
|
||||||
|
// ) : null}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|||||||
@ -1,41 +1,43 @@
|
|||||||
import { h } from 'preact';
|
// import { h } from 'preact';
|
||||||
import { useCallback, useState } from 'preact/hooks';
|
// import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
|
export {}
|
||||||
const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
|
|
||||||
|
|
||||||
const handleSelected = useCallback(
|
// export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
|
||||||
(index) => () => {
|
// const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
|
||||||
setSelectedIndex(index);
|
|
||||||
onChange && onChange(index);
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const RenderChildren = useCallback(() => {
|
// const handleSelected = useCallback(
|
||||||
return children.map((child, i) => {
|
// (index) => () => {
|
||||||
child.props.selected = i === selectedIndex;
|
// setSelectedIndex(index);
|
||||||
child.props.onClick = handleSelected(i);
|
// onChange && onChange(index);
|
||||||
return child;
|
// },
|
||||||
});
|
// [onChange]
|
||||||
}, [selectedIndex, children, handleSelected]);
|
// );
|
||||||
|
|
||||||
return (
|
// const RenderChildren = useCallback(() => {
|
||||||
<div className={`flex ${className}`}>
|
// return children.map((child, i) => {
|
||||||
<RenderChildren />
|
// child.props.selected = i === selectedIndex;
|
||||||
</div>
|
// child.props.onClick = handleSelected(i);
|
||||||
);
|
// return child;
|
||||||
}
|
// });
|
||||||
|
// }, [selectedIndex, children, handleSelected]);
|
||||||
|
|
||||||
export function TextTab({ selected, text, onClick, disabled }) {
|
// return (
|
||||||
const selectedStyle = disabled
|
// <div className={`flex ${className}`}>
|
||||||
? 'text-gray-400 dark:text-gray-600 bg-transparent'
|
// <RenderChildren />
|
||||||
: selected
|
// </div>
|
||||||
? '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}`}>
|
// export function TextTab({ selected, text, onClick, disabled }) {
|
||||||
<span>{text}</span>
|
// const selectedStyle = disabled
|
||||||
</button>
|
// ? '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 {
|
// interface IProp {
|
||||||
/** The time to calculate time-ago from */
|
// /** The time to calculate time-ago from */
|
||||||
time: Date;
|
// time: Date;
|
||||||
/** OPTIONAL: overwrite current time */
|
// /** OPTIONAL: overwrite current time */
|
||||||
currentTime?: Date;
|
// currentTime?: Date;
|
||||||
/** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
|
// /** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
|
||||||
dense?: boolean;
|
// dense?: boolean;
|
||||||
/** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
|
// /** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
|
||||||
refreshInterval?: number;
|
// refreshInterval?: number;
|
||||||
}
|
// }
|
||||||
|
|
||||||
type TimeUnit = {
|
export {}
|
||||||
unit: string;
|
|
||||||
full: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
|
// type TimeUnit = {
|
||||||
if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
|
// unit: string;
|
||||||
|
// full: string;
|
||||||
|
// value: number;
|
||||||
|
// };
|
||||||
|
|
||||||
const pastTime: Date = new Date(time);
|
// const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
|
||||||
const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
|
// if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
|
||||||
|
|
||||||
const timeUnits: TimeUnit[] = [
|
// const pastTime: Date = new Date(time);
|
||||||
{ unit: 'yr', full: 'year', value: 31536000 },
|
// const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
|
||||||
{ 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 elapsed: number = elapsedTime / 1000;
|
// const timeUnits: TimeUnit[] = [
|
||||||
if (elapsed < 10) {
|
// { unit: 'yr', full: 'year', value: 31536000 },
|
||||||
return 'just now';
|
// { 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++) {
|
// const elapsed: number = elapsedTime / 1000;
|
||||||
// if months
|
// if (elapsed < 10) {
|
||||||
if (i === 1) {
|
// return 'just now';
|
||||||
// Get the month and year for the time provided
|
// }
|
||||||
const pastMonth = pastTime.getUTCMonth();
|
|
||||||
const pastYear = pastTime.getUTCFullYear();
|
|
||||||
|
|
||||||
// get current month and year
|
// for (let i = 0; i < timeUnits.length; i++) {
|
||||||
const currentMonth = currentTime.getUTCMonth();
|
// // if months
|
||||||
const currentYear = currentTime.getUTCFullYear();
|
// 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.
|
// let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
|
||||||
if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
|
|
||||||
monthDiff--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monthDiff > 0) {
|
// // check if the time provided is the previous month but not exceeded 1 month ago.
|
||||||
const unitAmount = monthDiff;
|
// if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
|
||||||
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
// monthDiff--;
|
||||||
}
|
// }
|
||||||
} 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 TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
|
// if (monthDiff > 0) {
|
||||||
const [currentTime, setCurrentTime] = useState<Date>(new Date());
|
// const unitAmount = monthDiff;
|
||||||
useEffect(() => {
|
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||||
const intervalId: NodeJS.Timeout = setInterval(() => {
|
// }
|
||||||
setCurrentTime(new Date());
|
// } else if (elapsed >= timeUnits[i].value) {
|
||||||
}, refreshInterval);
|
// const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
|
||||||
return () => clearInterval(intervalId);
|
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
|
||||||
}, [refreshInterval]);
|
// }
|
||||||
|
// }
|
||||||
|
// 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>;
|
// const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
|
||||||
};
|
|
||||||
export default TimeAgo;
|
// return <span>{timeAgoValue}</span>;
|
||||||
|
// };
|
||||||
|
// export default TimeAgo;
|
||||||
|
|||||||
@ -1,65 +1,67 @@
|
|||||||
import { Fragment, h } from 'preact';
|
// import { Fragment, h } from 'preact';
|
||||||
import { useState } from 'preact/hooks';
|
// import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
|
export {}
|
||||||
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);
|
|
||||||
|
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
// export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
|
||||||
const getHoverStyle = () => {
|
// const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100);
|
||||||
if (boxLeftEdge < 15) {
|
// const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100);
|
||||||
// show object stats on right side
|
// const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100);
|
||||||
return {
|
// const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100);
|
||||||
left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
|
||||||
top: `${boxTopEdge}%`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// const [isHovering, setIsHovering] = useState(false);
|
||||||
right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
// const getHoverStyle = () => {
|
||||||
top: `${boxTopEdge}%`,
|
// if (boxLeftEdge < 15) {
|
||||||
};
|
// // show object stats on right side
|
||||||
};
|
// return {
|
||||||
|
// left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||||
|
// top: `${boxTopEdge}%`,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
const getObjectArea = () => {
|
// return {
|
||||||
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
// right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
|
||||||
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
// top: `${boxTopEdge}%`,
|
||||||
return Math.round(width * height);
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
const getObjectRatio = () => {
|
// const getObjectArea = () => {
|
||||||
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||||
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||||
return Math.round(100 * (width / height)) / 100;
|
// return Math.round(width * height);
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
// const getObjectRatio = () => {
|
||||||
<Fragment>
|
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
|
||||||
<div
|
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
|
||||||
className="absolute border-4 border-red-600"
|
// return Math.round(100 * (width / height)) / 100;
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
// };
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
|
||||||
onTouchStart={() => setIsHovering(true)}
|
// return (
|
||||||
onTouchEnd={() => setIsHovering(false)}
|
// <Fragment>
|
||||||
style={{
|
// <div
|
||||||
left: `${boxLeftEdge}%`,
|
// className="absolute border-4 border-red-600"
|
||||||
top: `${boxTopEdge}%`,
|
// onMouseEnter={() => setIsHovering(true)}
|
||||||
right: `${boxRightEdge}%`,
|
// onMouseLeave={() => setIsHovering(false)}
|
||||||
bottom: `${boxBottomEdge}%`,
|
// onTouchStart={() => setIsHovering(true)}
|
||||||
}}
|
// onTouchEnd={() => setIsHovering(false)}
|
||||||
>
|
// style={{
|
||||||
{eventOverlay.class_type == 'entered_zone' ? (
|
// left: `${boxLeftEdge}%`,
|
||||||
<div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
|
// top: `${boxTopEdge}%`,
|
||||||
) : null}
|
// right: `${boxRightEdge}%`,
|
||||||
</div>
|
// bottom: `${boxBottomEdge}%`,
|
||||||
{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>
|
// {eventOverlay.class_type == 'entered_zone' ? (
|
||||||
<div>{`Ratio: ${getObjectRatio()}`}</div>
|
// <div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
|
||||||
</div>
|
// ) : null}
|
||||||
)}
|
// </div>
|
||||||
</Fragment>
|
// {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 { h } from 'preact';
|
||||||
import useSWR from 'swr';
|
// import useSWR from 'swr';
|
||||||
import ActivityIndicator from './ActivityIndicator';
|
// import ActivityIndicator from './ActivityIndicator';
|
||||||
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
// import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
||||||
import About from '../icons/About';
|
// import About from '../icons/About';
|
||||||
import ActiveObjectIcon from '../icons/ActiveObject';
|
// import ActiveObjectIcon from '../icons/ActiveObject';
|
||||||
import PlayIcon from '../icons/Play';
|
// import PlayIcon from '../icons/Play';
|
||||||
import ExitIcon from '../icons/Exit';
|
// import ExitIcon from '../icons/Exit';
|
||||||
import StationaryObjectIcon from '../icons/StationaryObject';
|
// import StationaryObjectIcon from '../icons/StationaryObject';
|
||||||
import FaceIcon from '../icons/Face';
|
// import FaceIcon from '../icons/Face';
|
||||||
import LicensePlateIcon from '../icons/LicensePlate';
|
// import LicensePlateIcon from '../icons/LicensePlate';
|
||||||
import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
// import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
||||||
import ZoneIcon from '../icons/Zone';
|
// import ZoneIcon from '../icons/Zone';
|
||||||
import { useMemo, useState } from 'preact/hooks';
|
// import { useMemo, useState } from 'preact/hooks';
|
||||||
import Button from './Button';
|
// import Button from './Button';
|
||||||
|
|
||||||
export default function TimelineSummary({ event, onFrameSelected }) {
|
export {}
|
||||||
const { data: eventTimeline } = useSWR([
|
|
||||||
'timeline',
|
|
||||||
{
|
|
||||||
source_id: event.id,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { data: config } = useSWR('config');
|
// export default function TimelineSummary({ event, onFrameSelected }) {
|
||||||
|
// const { data: eventTimeline } = useSWR([
|
||||||
|
// 'timeline',
|
||||||
|
// {
|
||||||
|
// source_id: event.id,
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
|
||||||
const annotationOffset = useMemo(() => {
|
// const { data: config } = useSWR('config');
|
||||||
if (!config) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
|
// const annotationOffset = useMemo(() => {
|
||||||
}, [config, event]);
|
// if (!config) {
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
|
||||||
const [timeIndex, setTimeIndex] = useState(-1);
|
// return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
|
||||||
|
// }, [config, event]);
|
||||||
|
|
||||||
const recordingParams = useMemo(() => {
|
// const [timeIndex, setTimeIndex] = useState(-1);
|
||||||
if (!event.end_time) {
|
|
||||||
return {
|
|
||||||
after: event.start_time,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// const recordingParams = useMemo(() => {
|
||||||
before: event.end_time,
|
// if (!event.end_time) {
|
||||||
after: event.start_time,
|
// return {
|
||||||
};
|
// after: event.start_time,
|
||||||
}, [event]);
|
// };
|
||||||
const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
|
// }
|
||||||
|
|
||||||
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
// return {
|
||||||
const getSeekSeconds = (seekUnix) => {
|
// before: event.end_time,
|
||||||
if (!recordings) {
|
// after: event.start_time,
|
||||||
return 0;
|
// };
|
||||||
}
|
// }, [event]);
|
||||||
|
// const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
|
||||||
|
|
||||||
let seekSeconds = 0;
|
// // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||||
recordings.every((segment) => {
|
// const getSeekSeconds = (seekUnix) => {
|
||||||
// if the next segment is past the desired time, stop calculating
|
// if (!recordings) {
|
||||||
if (segment.start_time > seekUnix) {
|
// return 0;
|
||||||
return false;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.end_time < seekUnix) {
|
// let seekSeconds = 0;
|
||||||
seekSeconds += segment.end_time - segment.start_time;
|
// recordings.every((segment) => {
|
||||||
return true;
|
// // 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);
|
// if (segment.end_time < seekUnix) {
|
||||||
return true;
|
// 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) => {
|
// return seekSeconds;
|
||||||
setTimeIndex(index);
|
// };
|
||||||
onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!eventTimeline || !config) {
|
// const onSelectMoment = async (index) => {
|
||||||
return <ActivityIndicator />;
|
// setTimeIndex(index);
|
||||||
}
|
// onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
|
||||||
|
// };
|
||||||
|
|
||||||
if (eventTimeline.length == 0) {
|
// if (!eventTimeline || !config) {
|
||||||
return <div />;
|
// return <ActivityIndicator />;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
// if (eventTimeline.length == 0) {
|
||||||
<div className="flex flex-col">
|
// return <div />;
|
||||||
<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 getTimelineIcon(timelineItem) {
|
// return (
|
||||||
switch (timelineItem.class_type) {
|
// <div className="flex flex-col">
|
||||||
case 'visible':
|
// <div className="h-14 flex justify-center">
|
||||||
return <PlayIcon className="w-8" />;
|
// <div className="flex flex-row flex-nowrap justify-between overflow-auto">
|
||||||
case 'gone':
|
// {eventTimeline.map((item, index) => (
|
||||||
return <ExitIcon className="w-8" />;
|
// <Button
|
||||||
case 'active':
|
// key={index}
|
||||||
return <ActiveObjectIcon className="w-8" />;
|
// className="rounded-full"
|
||||||
case 'stationary':
|
// type="iconOnly"
|
||||||
return <StationaryObjectIcon className="w-8" />;
|
// color={index == timeIndex ? 'blue' : 'gray'}
|
||||||
case 'entered_zone':
|
// aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
||||||
return <ZoneIcon className="w-8" />;
|
// onClick={() => onSelectMoment(index)}
|
||||||
case 'attribute':
|
// >
|
||||||
switch (timelineItem.data.attribute) {
|
// {getTimelineIcon(item)}
|
||||||
case 'face':
|
// </Button>
|
||||||
return <FaceIcon className="w-8" />;
|
// ))}
|
||||||
case 'license_plate':
|
// </div>
|
||||||
return <LicensePlateIcon className="w-8" />;
|
// </div>
|
||||||
default:
|
// {timeIndex >= 0 ? (
|
||||||
return <DeliveryTruckIcon className="w-8" />;
|
// <div className="m-2 max-w-md self-center">
|
||||||
}
|
// <div className="flex justify-start">
|
||||||
case 'sub_label':
|
// <div className="text-lg flex justify-between py-4">Bounding boxes may not align</div>
|
||||||
switch (timelineItem.data.label) {
|
// <Button
|
||||||
case 'person':
|
// className="rounded-full"
|
||||||
return <FaceIcon className="w-8" />;
|
// type="text"
|
||||||
case 'car':
|
// color="gray"
|
||||||
return <LicensePlateIcon className="w-8" />;
|
// 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) {
|
// function getTimelineIcon(timelineItem) {
|
||||||
switch (timelineItem.class_type) {
|
// switch (timelineItem.class_type) {
|
||||||
case 'visible':
|
// case 'visible':
|
||||||
return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
// return <PlayIcon className="w-8" />;
|
||||||
date_style: 'short',
|
// case 'gone':
|
||||||
time_style: 'medium',
|
// return <ExitIcon className="w-8" />;
|
||||||
time_format: config.ui.time_format,
|
// case 'active':
|
||||||
})}`;
|
// return <ActiveObjectIcon className="w-8" />;
|
||||||
case 'entered_zone':
|
// case 'stationary':
|
||||||
return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
// return <StationaryObjectIcon className="w-8" />;
|
||||||
.join(' and ')
|
// case 'entered_zone':
|
||||||
.replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
// return <ZoneIcon className="w-8" />;
|
||||||
date_style: 'short',
|
// case 'attribute':
|
||||||
time_style: 'medium',
|
// switch (timelineItem.data.attribute) {
|
||||||
time_format: config.ui.time_format,
|
// case 'face':
|
||||||
})}`;
|
// return <FaceIcon className="w-8" />;
|
||||||
case 'active':
|
// case 'license_plate':
|
||||||
return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
// return <LicensePlateIcon className="w-8" />;
|
||||||
date_style: 'short',
|
// default:
|
||||||
time_style: 'medium',
|
// return <DeliveryTruckIcon className="w-8" />;
|
||||||
time_format: config.ui.time_format,
|
// }
|
||||||
})}`;
|
// case 'sub_label':
|
||||||
case 'stationary':
|
// switch (timelineItem.data.label) {
|
||||||
return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
// case 'person':
|
||||||
date_style: 'short',
|
// return <FaceIcon className="w-8" />;
|
||||||
time_style: 'medium',
|
// case 'car':
|
||||||
time_format: config.ui.time_format,
|
// return <LicensePlateIcon className="w-8" />;
|
||||||
})}`;
|
// }
|
||||||
case 'attribute': {
|
// }
|
||||||
let title = "";
|
// }
|
||||||
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
|
|
||||||
title = `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label}`;
|
// function getTimelineItemDescription(config, timelineItem, event) {
|
||||||
} else {
|
// switch (timelineItem.class_type) {
|
||||||
title = `${event.label} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`
|
// case 'visible':
|
||||||
}
|
// return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
return `${title} at ${formatUnixTimestampToDateTime(
|
// date_style: 'short',
|
||||||
timelineItem.timestamp,
|
// time_style: 'medium',
|
||||||
{
|
// time_format: config.ui.time_format,
|
||||||
date_style: 'short',
|
// })}`;
|
||||||
time_style: 'medium',
|
// case 'entered_zone':
|
||||||
time_format: config.ui.time_format,
|
// return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
||||||
}
|
// .join(' and ')
|
||||||
)}`;
|
// .replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
}
|
// date_style: 'short',
|
||||||
case 'sub_label':
|
// time_style: 'medium',
|
||||||
return `${event.label} recognized as ${timelineItem.data.sub_label} at ${formatUnixTimestampToDateTime(
|
// time_format: config.ui.time_format,
|
||||||
timelineItem.timestamp,
|
// })}`;
|
||||||
{
|
// case 'active':
|
||||||
date_style: 'short',
|
// return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
time_style: 'medium',
|
// date_style: 'short',
|
||||||
time_format: config.ui.time_format,
|
// time_style: 'medium',
|
||||||
}
|
// time_format: config.ui.time_format,
|
||||||
)}`;
|
// })}`;
|
||||||
case 'gone':
|
// case 'stationary':
|
||||||
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
// return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
date_style: 'short',
|
// date_style: 'short',
|
||||||
time_style: 'medium',
|
// time_style: 'medium',
|
||||||
time_format: config.ui.time_format,
|
// 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 { createPortal } from 'react'; // TODO implement
|
||||||
import { useLayoutEffect, useRef, useState } from 'react';
|
// import { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
const TIP_SPACE = 20;
|
// const TIP_SPACE = 20;
|
||||||
|
|
||||||
export default function Tooltip({ relativeTo, text, capitalize }) {
|
export {}
|
||||||
const [position, setPosition] = useState({ top: -9999, left: -9999 });
|
|
||||||
const portalRoot = document.getElementById('tooltips');
|
|
||||||
const ref = useRef();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
// export default function Tooltip({ relativeTo, text, capitalize }) {
|
||||||
if (ref && ref.current && relativeTo && relativeTo.current) {
|
// const [position, setPosition] = useState({ top: -9999, left: -9999 });
|
||||||
const windowWidth = window.innerWidth;
|
// const portalRoot = document.getElementById('tooltips');
|
||||||
const {
|
// const ref = useRef();
|
||||||
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;
|
|
||||||
|
|
||||||
const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
|
// useLayoutEffect(() => {
|
||||||
const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
|
// 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;
|
// const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
|
||||||
let newLeft = left - Math.round(tipWidth / 2);
|
// const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPosition({ left: newLeft, top: newTop });
|
// let newTop = top - TIP_SPACE - tipHeight;
|
||||||
}
|
// let newLeft = left - Math.round(tipWidth / 2);
|
||||||
}, [relativeTo, ref]);
|
// // 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 = (
|
// setPosition({ left: newLeft, top: newTop });
|
||||||
<div
|
// }
|
||||||
role="tooltip"
|
// }, [relativeTo, ref]);
|
||||||
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;
|
// 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<
|
// const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
// HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<div
|
// <div
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
// className={cn(
|
// // className={cn(
|
||||||
// "rounded-lg border bg-card text-card-foreground shadow-sm",
|
// // "rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
// className
|
// // className
|
||||||
// )}
|
// // )}
|
||||||
{...props}
|
// {...props}
|
||||||
/>
|
// />
|
||||||
))
|
// ))
|
||||||
Card.displayName = "Card"
|
// Card.displayName = "Card"
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
// const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
// HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<div
|
// <div
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
// className={cn("flex flex-col space-y-1.5 p-6", className)}
|
// // className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
// {...props}
|
||||||
/>
|
// />
|
||||||
))
|
// ))
|
||||||
CardHeader.displayName = "CardHeader"
|
// CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
// const CardTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
// HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
// React.HTMLAttributes<HTMLHeadingElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<h3
|
// <h3
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
// className={cn(
|
// // className={cn(
|
||||||
// "text-2xl font-semibold leading-none tracking-tight",
|
// // "text-2xl font-semibold leading-none tracking-tight",
|
||||||
// className
|
// // className
|
||||||
// )}
|
// // )}
|
||||||
{...props}
|
// {...props}
|
||||||
/>
|
// />
|
||||||
))
|
// ))
|
||||||
CardTitle.displayName = "CardTitle"
|
// CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
// const CardDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
// HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
// React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<p
|
// <p
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
// className={cn("text-sm text-muted-foreground", className)}
|
// // className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
// {...props}
|
||||||
/>
|
// />
|
||||||
))
|
// ))
|
||||||
CardDescription.displayName = "CardDescription"
|
// CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
// const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
// HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<div ref={ref}
|
// <div ref={ref}
|
||||||
// className= {cn("p-6 pt-0", className)}
|
// // className= {cn("p-6 pt-0", className)}
|
||||||
{...props} />
|
// {...props} />
|
||||||
))
|
// ))
|
||||||
CardContent.displayName = "CardContent"
|
// CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
// const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
// HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
// React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
// >(({ className, ...props }, ref) => (
|
||||||
<div
|
// <div
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
// className={cn("flex items-center p-6 pt-0", className)}
|
// // className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
// {...props}
|
||||||
/>
|
// />
|
||||||
))
|
// ))
|
||||||
CardFooter.displayName = "CardFooter"
|
// 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 React from 'react';
|
||||||
import { Center, DEFAULT_THEME } from '@mantine/core';
|
import { Center, DEFAULT_THEME } from '@mantine/core';
|
||||||
import CogwheelSVG from './svg/CogwheelSVG';
|
import CogwheelSVG from '../svg/CogwheelSVG';
|
||||||
|
|
||||||
|
|
||||||
const CogwheelLoader = () => {
|
const CogwheelLoader = () => {
|
||||||
@ -15,7 +15,7 @@ export class RecordingsStore {
|
|||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
recordingSchema = z.object({
|
private _recordingSchema = z.object({
|
||||||
hostName: z.string(),
|
hostName: z.string(),
|
||||||
cameraName: z.string(),
|
cameraName: z.string(),
|
||||||
hour: z.string(),
|
hour: z.string(),
|
||||||
@ -31,7 +31,7 @@ export class RecordingsStore {
|
|||||||
this._recordToPlay = value
|
this._recordToPlay = value
|
||||||
}
|
}
|
||||||
getFullRecordForPlay(value: RecordForPlay) {
|
getFullRecordForPlay(value: RecordForPlay) {
|
||||||
return this.recordingSchema.safeParse(value)
|
return this._recordingSchema.safeParse(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hostIdParam: string | undefined
|
private _hostIdParam: string | undefined
|
||||||
@ -63,6 +63,11 @@ export class RecordingsStore {
|
|||||||
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
|
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
|
||||||
this._selectedCamera = value
|
this._selectedCamera = value
|
||||||
}
|
}
|
||||||
selectedStartDay: string = ''
|
private _selectedRange: [Date | null, Date | null] = [null, null]
|
||||||
selectedEndDay: string = ''
|
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 = {
|
export const strings = {
|
||||||
host: {
|
host: 'Хост',
|
||||||
|
hostArr: {
|
||||||
name: 'Имя хоста',
|
name: 'Имя хоста',
|
||||||
url: 'Адрес',
|
url: 'Адрес',
|
||||||
enabled: 'Включен',
|
enabled: 'Включен',
|
||||||
},
|
},
|
||||||
// user section
|
player: {
|
||||||
|
startVideo: 'Вкл Видео',
|
||||||
|
stopVideo: 'Выкл Видео',
|
||||||
|
object: 'Объект',
|
||||||
|
duration: 'Длительность',
|
||||||
|
startTime: 'Начало',
|
||||||
|
endTime: 'Конец'
|
||||||
|
},
|
||||||
|
camera: 'Камера',
|
||||||
|
hour: 'Час',
|
||||||
|
minute: 'Минута',
|
||||||
|
second: 'Секунда',
|
||||||
|
events: 'События',
|
||||||
|
event: 'Событие',
|
||||||
|
notHaveEvents: 'Нет событий',
|
||||||
|
date: 'Дата',
|
||||||
|
day: 'День',
|
||||||
|
selectHost:'Выбери хост',
|
||||||
|
selectCamera: 'Выбери камеру',
|
||||||
|
selectDate: 'Выбери дату',
|
||||||
|
selectRange: 'Выбери период',
|
||||||
aboutMe: "Обо мне",
|
aboutMe: "Обо мне",
|
||||||
settings: "Настройки",
|
settings: "Настройки",
|
||||||
changeTheme: "Изменить тему",
|
changeTheme: "Изменить тему",
|
||||||
|
|||||||
@ -30,6 +30,33 @@ export const unixTimeToDate = (unixTime: number) => {
|
|||||||
return formattedDate;
|
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 day frigate format, e.g day: 2024-02-23
|
||||||
* @param hour frigate format, e.g hour: 22
|
* @param hour frigate format, e.g hour: 22
|
||||||
@ -210,14 +237,14 @@ interface DurationToken {
|
|||||||
* @param end_time: number|null - Unix timestamp for end time
|
* @param end_time: number|null - Unix timestamp for end time
|
||||||
* @returns string - duration or 'In Progress' if end time is not provided
|
* @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)) {
|
if (isNaN(start_time)) {
|
||||||
return 'Invalid start time';
|
return
|
||||||
}
|
}
|
||||||
let duration = 'In Progress';
|
let duration = 'In Progress';
|
||||||
if (end_time) {
|
if (end_time) {
|
||||||
if (isNaN(end_time)) {
|
if (isNaN(end_time)) {
|
||||||
return 'Invalid end time';
|
return
|
||||||
}
|
}
|
||||||
const start = fromUnixTime(start_time);
|
const start = fromUnixTime(start_time);
|
||||||
const end = fromUnixTime(end_time);
|
const end = fromUnixTime(end_time);
|
||||||
@ -61,9 +61,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headTitle = [
|
const headTitle = [
|
||||||
{ propertyName: 'name', title: strings.host.name },
|
{ propertyName: 'name', title: strings.hostArr.name },
|
||||||
{ propertyName: 'host', title: strings.host.url },
|
{ propertyName: 'host', title: strings.hostArr.url },
|
||||||
{ propertyName: 'enabled', title: strings.host.enabled },
|
{ propertyName: 'enabled', title: strings.hostArr.enabled },
|
||||||
{ title: '', sorting: false },
|
{ title: '', sorting: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import JSMpegPlayer from './JSMpegPlayer';
|
import JSMpegPlayer from '../shared/components/players/JSMpegPlayer';
|
||||||
import MSEPlayer from './MsePlayer';
|
import MSEPlayer from '../shared/components/players/MsePlayer';
|
||||||
import { CameraConfig } from '../../../types/frigateConfig';
|
import { CameraConfig } from '../types/frigateConfig';
|
||||||
import { LivePlayerMode } from '../../../types/live';
|
import { LivePlayerMode } from '../types/live';
|
||||||
import useCameraActivity from '../../../hooks/use-camera-activity';
|
import useCameraActivity from '../hooks/use-camera-activity';
|
||||||
import useCameraLiveMode from '../../../hooks/use-camera-live-mode';
|
import useCameraLiveMode from '../hooks/use-camera-live-mode';
|
||||||
import WebRtcPlayer from './WebRTCPlayer';
|
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer';
|
||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||||
import { GetCameraWHostWConfig } from '../../../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||||
|
|
||||||
type LivePlayerProps = {
|
type LivePlayerProps = {
|
||||||
camera: GetCameraWHostWConfig;
|
camera: GetCameraWHostWConfig;
|
||||||
@ -70,7 +70,7 @@ const Player = ({
|
|||||||
player = (
|
player = (
|
||||||
<MSEPlayer
|
<MSEPlayer
|
||||||
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
|
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
|
||||||
camera='Not yet implemented'
|
camera='Not yet implemented' // TODO implement player
|
||||||
playbackEnabled={cameraActive}
|
playbackEnabled={cameraActive}
|
||||||
onPlaying={() => setLiveReady(true)}
|
onPlaying={() => setLiveReady(true)}
|
||||||
wsUrl={wsUrl}
|
wsUrl={wsUrl}
|
||||||
@ -1,12 +1,9 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Context } from '..';
|
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 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 {
|
interface RecordingsFiltersRightSideProps {
|
||||||
}
|
}
|
||||||
@ -15,58 +12,18 @@ const RecordingsFiltersRightSide = ({
|
|||||||
}: RecordingsFiltersRightSideProps) => {
|
}: RecordingsFiltersRightSideProps) => {
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
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')
|
console.log('RecordingsFiltersRightSide rendered')
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OneSelectFilter
|
<HostSelectFilter />
|
||||||
id='frigate-hosts'
|
|
||||||
label='Select host:'
|
|
||||||
spaceBetween='1rem'
|
|
||||||
value={recStore.selectedHost?.id || ''}
|
|
||||||
defaultValue={recStore.selectedHost?.id || ''}
|
|
||||||
data={hostItems}
|
|
||||||
onChange={handleSelect}
|
|
||||||
/>
|
|
||||||
{recStore.selectedHost ?
|
{recStore.selectedHost ?
|
||||||
<CameraSelectFilter
|
<CameraSelectFilter
|
||||||
selectedHostId={recStore.selectedHost.id} />
|
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 { Context } from '..';
|
||||||
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||||
import { host } from '../shared/env.const';
|
import { host } from '../shared/env.const';
|
||||||
import CogwheelLoader from '../shared/components/CogwheelLoader';
|
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||||
import RetryError from '../pages/RetryError';
|
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||||
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
|
|
||||||
interface SelectedCameraListProps {
|
interface SelectedCameraListProps {
|
||||||
cameraId: string,
|
// cameraId: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectedCameraList = ({
|
const SelectedCameraList = ({
|
||||||
cameraId,
|
// cameraId,
|
||||||
}: SelectedCameraListProps) => {
|
}: SelectedCameraListProps) => {
|
||||||
|
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
|
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getCameraWHost, cameraId],
|
queryKey: [frigateQueryKeys.getCameraWHost, recStore.selectedCamera?.id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (cameraId) {
|
if (recStore.selectedCamera) {
|
||||||
return frigateApi.getCameraWHost(cameraId)
|
return frigateApi.getCameraWHost(recStore.selectedCamera.id)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -33,14 +34,18 @@ const SelectedCameraList = ({
|
|||||||
cameraRefetch()
|
cameraRefetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cameraPending) return <CogwheelLoader />
|
if (cameraPending) return <CenterLoader />
|
||||||
if (cameraError) return <RetryError onRetry={handleRetry} />
|
if (cameraError) return <RetryErrorPage onRetry={handleRetry} />
|
||||||
|
|
||||||
if (!camera?.frigateHost) return null
|
if (!camera?.frigateHost) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||||
<Text>{camera.frigateHost.name} / {camera.name}</Text>
|
<Text>{camera.frigateHost.name} / {camera.name}</Text>
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<CameraAccordion camera={camera} host={camera.frigateHost} />
|
<CameraAccordion camera={camera} host={camera.frigateHost} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
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'));
|
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
|
||||||
|
|
||||||
|
|
||||||
@ -39,14 +40,14 @@ const SelectedHostList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hostPending) return <CenterLoader />
|
if (hostPending) return <CenterLoader />
|
||||||
if (hostError) return <RetryError onRetry={handleRetry} />
|
if (hostError) return <RetryErrorPage onRetry={handleRetry} />
|
||||||
|
|
||||||
if (!host || host.cameras.length < 1) return null
|
if (!host || host.cameras.length < 1) return null
|
||||||
|
|
||||||
const cameras = host.cameras.slice(0, 2).map(camera => {
|
const cameras = host.cameras.slice(0, 2).map(camera => {
|
||||||
return (
|
return (
|
||||||
<Accordion.Item key={camera.id + 'Item'} value={camera.id}>
|
<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'}>
|
<Accordion.Panel key={camera.id + 'Panel'}>
|
||||||
{openCameraId === camera.id && (
|
{openCameraId === camera.id && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
@ -60,7 +61,7 @@ const SelectedHostList = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||||
<Text>{host.name}</Text>
|
<Text>{strings.host}: {host.name}</Text>
|
||||||
<Accordion
|
<Accordion
|
||||||
mt='1rem'
|
mt='1rem'
|
||||||
variant='separated'
|
variant='separated'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user