refactoring

add translates
finish recordings
This commit is contained in:
NlightN22 2024-02-25 21:15:31 +07:00
parent a971ea55a4
commit 7304fe231e
69 changed files with 1682 additions and 1584 deletions

View File

@ -1,6 +1,6 @@
import { Button, Flex, Text } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import CogWheelWithText from '../shared/components/CogWheelWithText';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { redirect, useNavigate } from 'react-router-dom';
import { routesPath } from '../router/routes.path';

View File

@ -1,6 +1,6 @@
import { Button, Flex, Text } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import CogWheelWithText from '../shared/components/CogWheelWithText';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { redirect, useNavigate } from 'react-router-dom';
import { routesPath } from '../router/routes.path';

View File

@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import RetryErrorPage from './RetryErrorPage';
import { Context } from '..';
import { strings } from '../shared/strings/strings';
import { Button, Flex } from '@mantine/core';
@ -71,7 +71,7 @@ const FrigateHostsPage = observer(() => {
}
if (hostsPending) return <CenterLoader />
if (hostsError) return <RetryError />
if (hostsError) return <RetryErrorPage />
return (
<div>
{

View File

@ -9,7 +9,7 @@ import { configureMonacoYaml } from "monaco-yaml";
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
import * as monaco from "monaco-editor";
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import RetryErrorPage from './RetryErrorPage';
const HostConfigPage = () => {
@ -74,7 +74,7 @@ const HostConfigPage = () => {
if (configPending) return <CenterLoader />
if (configError) return <RetryError onRetry={refetch} />
if (configError) return <RetryErrorPage onRetry={refetch} />
return (
<Flex direction='column' h='100%' w='100%' justify='stretch'>

View File

@ -5,8 +5,8 @@ import { useParams } from 'react-router-dom';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { useQuery } from '@tanstack/react-query';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import Player from '../shared/components/frigate/Player';
import RetryErrorPage from './RetryErrorPage';
import Player from '../widgets/Player';
import { Flex } from '@mantine/core';
const LiveCameraPage = observer(() => {
@ -28,7 +28,7 @@ const LiveCameraPage = observer(() => {
if (isPending) return <CenterLoader />
if (isError) return <RetryError onRetry={refetch} />
if (isError) return <RetryErrorPage onRetry={refetch} />
return (
<Flex w='100%' h='100%' justify='center'>

View File

@ -8,11 +8,12 @@ import { observer } from 'mobx-react-lite'
import CenterLoader from '../shared/components/CenterLoader';
import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import RetryError from './RetryError';
import RetryErrorPage from './RetryErrorPage';
import CameraCard from '../shared/components/CameraCard';
const MainBody = observer(() => {
const MainPage = () => {
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
@ -32,11 +33,11 @@ const MainBody = observer(() => {
if (isPending) return <CenterLoader />
if (isError) return <RetryError onRetry={refetch} />
if (isError) return <RetryErrorPage onRetry={refetch} />
const cards = () => {
// return cameras.filter(cam => cam.frigateHost?.host.includes('5001')).slice(0,1).map(camera => (
return cameras.map(camera => (
return cameras.filter(cam => cam.frigateHost?.host.includes('5000')).slice(0,25).map(camera => (
// return cameras.map(camera => (
<CameraCard
key={camera.id}
camera={camera}
@ -45,7 +46,7 @@ const MainBody = observer(() => {
}
return (
<Flex direction='column' h='100%'>
<Flex direction='column' h='100%' >
<Flex justify='space-between' align='center' w='100%'>
<Group
w='25%'
@ -63,13 +64,13 @@ const MainBody = observer(() => {
<ViewSelector state={viewState} onChange={handleToggleState} />
</Group>
</Flex>
<Flex justify='center' h='100%' direction='column'>
<Flex justify='center' h='100%' direction='column' >
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
{cards()}
</Grid>
</Flex>
</Flex>
);
})
}
export default MainBody;
export default observer(MainPage);

View File

@ -9,12 +9,18 @@ import SelectedCameraList from '../widgets/SelectedCameraList';
import SelectedHostList from '../widgets/SelectedHostList';
import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import SelecteDayList from '../widgets/SelecteDayList';
import { useDebouncedValue } from '@mantine/hooks';
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
import CenterLoader from '../shared/components/CenterLoader';
const recordingsQuery = {
export const recordingsPageQuery = {
hostId: 'hostId',
cameraId: 'cameraId',
date: 'date',
startDay: 'startDay',
endDay: 'endDay',
hour: 'hour',
}
@ -24,13 +30,16 @@ const RecordingsPage = observer(() => {
const location = useLocation()
const navigate = useNavigate()
const queryParams = new URLSearchParams(location.search)
const paramHostId = queryParams.get(recordingsQuery.hostId)
const paramCameraId = queryParams.get(recordingsQuery.cameraId);
const paramDate = queryParams.get(recordingsQuery.date);
const paramTime = queryParams.get(recordingsQuery.hour);
const paramHostId = queryParams.get(recordingsPageQuery.hostId)
const paramCameraId = queryParams.get(recordingsPageQuery.cameraId);
const paramStartDay = queryParams.get(recordingsPageQuery.startDay);
const paramEndDay = queryParams.get(recordingsPageQuery.endDay);
const paramTime = queryParams.get(recordingsPageQuery.hour);
const [hostId, setHostId] = useState<string>('')
const [cameraId, setCameraId] = useState<string>('')
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
const [firstRender, setFirstRender] = useState(false)
useEffect(() => {
sideBarsStore.rightVisible = true
@ -39,15 +48,21 @@ const RecordingsPage = observer(() => {
)
if (paramHostId) recStore.hostIdParam = paramHostId
if (paramCameraId) recStore.cameraIdParam = paramCameraId
if (paramStartDay && paramEndDay) {
const parsedStartDay = parseQueryDateToDate(paramStartDay)
const parsedEndDay = parseQueryDateToDate(paramEndDay)
recStore.selectedRange = [parsedStartDay, parsedEndDay]
}
setFirstRender(true)
return () => sideBarsStore.setRightChildren(null)
}, [])
useEffect(() => {
setHostId(recStore.selectedHost?.id || '')
if (recStore.selectedHost) {
queryParams.set(recordingsQuery.hostId, recStore.selectedHost.id)
queryParams.set(recordingsPageQuery.hostId, recStore.selectedHost.id)
} else {
queryParams.delete(recordingsQuery.hostId)
queryParams.delete(recordingsPageQuery.hostId)
}
navigate({ pathname: location.pathname, search: queryParams.toString() });
}, [recStore.selectedHost])
@ -55,26 +70,61 @@ const RecordingsPage = observer(() => {
useEffect(() => {
setCameraId(recStore.selectedCamera?.id || '')
if (recStore.selectedCamera) {
queryParams.set(recordingsQuery.cameraId, recStore.selectedCamera?.id)
queryParams.set(recordingsPageQuery.cameraId, recStore.selectedCamera?.id)
} else {
console.log('delete recordingsQuery.cameraId')
queryParams.delete(recordingsQuery.cameraId)
queryParams.delete(recordingsPageQuery.cameraId)
}
navigate({ pathname: location.pathname, search: queryParams.toString() });
}, [recStore.selectedCamera])
if (cameraId) {
return <SelectedCameraList cameraId={cameraId} />
useEffect(() => {
setPeriod(recStore.selectedRange)
const [startDay, endDay] = recStore.selectedRange
if (startDay && endDay) {
const startQuery = dateToQueryString(startDay)
const endQuery = dateToQueryString(endDay)
queryParams.set(recordingsPageQuery.startDay, startQuery)
queryParams.set(recordingsPageQuery.endDay, endQuery)
} else {
queryParams.delete(recordingsPageQuery.startDay)
queryParams.delete(recordingsPageQuery.endDay)
}
navigate({ pathname: location.pathname, search: queryParams.toString() });
}, [recStore.selectedRange])
console.log('RecordingsPage rendered')
if (!firstRender) return <CenterLoader />
const [startDay, endDay] = period
if (startDay && endDay) {
if (startDay.getDate() === endDay.getDate()) { // if select only one day
return <SelecteDayList day={startDay} />
}
}
if (hostId) {
if (cameraId && paramCameraId) {
// console.log('cameraId', cameraId)
// console.log('paramCameraId', paramCameraId)
if ((startDay && endDay) || (!startDay && !endDay)) {
return <SelectedCameraList />
// return <SelectedCameraList cameraId={cameraId} />
}
}
if (hostId && paramHostId && !cameraId) {
return <SelectedHostList hostId={hostId} />
}
console.log('RecordingsPage rendered')
return (
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
<Text size='xl'>Please select host</Text>
{!hostId ?
<Text size='xl'>Please select host</Text>
: <></>}
{hostId && !(startDay && endDay) ?
<Text size='xl'>Please select date</Text>
: <></>
}
</Flex>
)
})

View File

@ -6,11 +6,11 @@ import { useNavigate } from 'react-router-dom';
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
import { Context } from '..';
interface RetryErrorProps {
interface RetryErrorPageProps {
onRetry?: () => void
}
const RetryError = ({ onRetry }: RetryErrorProps) => {
const RetryErrorPage = ({ onRetry }: RetryErrorPageProps) => {
const navigate = useNavigate()
const { sideBarsStore } = useContext(Context)
@ -48,4 +48,4 @@ const RetryError = ({ onRetry }: RetryErrorProps) => {
);
};
export default RetryError;
export default RetryErrorPage;

View File

@ -6,7 +6,7 @@ import {
} from '@tanstack/react-query'
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import RetryErrorPage from './RetryErrorPage';
import { Button, Flex, Space } from '@mantine/core';
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
import { strings } from '../shared/strings/strings';
@ -83,7 +83,7 @@ const SettingsPage = () => {
if (configPending) return <CenterLoader />
if (configError) return <RetryError onRetry={refetch} />
if (configError) return <RetryErrorPage onRetry={refetch} />
return (
<Flex h='100%'>

View File

@ -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
View 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
View 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;

View File

@ -1,8 +1,8 @@
import {JSX} from "react";
import Test from "../pages/Test"
import MainBody from "../pages/MainBody";
import Test from "../pages/TestPage"
import MainPage from "../pages/MainPage";
import {routesPath} from "./routes.path";
import RetryError from "../pages/RetryError";
import RetryErrorPage from "../pages/RetryErrorPage";
import Forbidden from "../pages/403";
import NotFound from "../pages/404";
import SettingsPage from "../pages/SettingsPage";
@ -53,11 +53,11 @@ export const routes: IRoute[] = [
},
{
path: routesPath.MAIN_PATH,
component: <MainBody />,
component: <MainPage />,
},
{
path: routesPath.RETRY_ERROR_PATH,
component: <RetryError />,
component: <RetryErrorPage />,
},
{
path: routesPath.FORBIDDEN_ERROR_PATH,

View File

@ -1,12 +1,13 @@
import { useEffect, useRef } from "react";
import { CameraConfig } from "../../../types/frigateConfig";
import { AspectRatio, Flex, createStyles, Text } from "@mantine/core";
import { CameraConfig } from "../../types/frigateConfig";
import { Flex, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import CenterLoader from "../CenterLoader";
import axios from "axios";
import { frigateApi, proxyApi } from "../../../services/frigate.proxy/frigate.api";
import CenterLoader from "./CenterLoader";
import { frigateApi, proxyApi } from "../../services/frigate.proxy/frigate.api";
import { useIntersection } from "@mantine/hooks";
import CogwheelLoader from "./loaders/CogwheelLoader";
interface CameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
interface AutoUpdatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
className?: string;
cameraConfig?: CameraConfig;
onload?: () => void;
@ -18,26 +19,29 @@ const AutoUpdatedImage = ({
imageUrl,
enabled,
...rest
}: CameraImageProps) => {
}: AutoUpdatedImageProps) => {
const { ref, entry } = useIntersection({threshold: 0.1,})
const isVisible = entry?.isIntersecting
const { data: imageBlob, refetch, isPending, isError } = useQuery({
queryKey: ['image', imageUrl],
queryFn: () => proxyApi.getImageFrigate(imageUrl),
staleTime: 60 * 1000,
gcTime: Infinity,
refetchInterval: 60 * 1000,
refetchInterval: isVisible ? 30 * 1000 : undefined,
});
useEffect(() => {
const intervalId = setInterval(() => {
refetch();
}, 60 * 1000);
if (isVisible) {
console.log('imageUrl is visible')
const intervalId = setInterval(() => {
refetch();
}, 60 * 1000);
return () => clearInterval(intervalId);
}
}, [refetch, isVisible]);
return () => clearInterval(intervalId);
}, [refetch]);
if (isPending) return <CenterLoader />
if (isPending) return <CogwheelLoader />
if (isError) return (
<Flex direction="column" justify="center" h="100%">
@ -51,7 +55,7 @@ const AutoUpdatedImage = ({
return (
<>
{enabled ? <img src={image} alt="Dynamic Content" {...rest}/>
{enabled ? <img ref={ref} src={image} alt="Dynamic Content" {...rest}/>
:
<Flex direction="column" justify="center" h="100%">
<Text align="center">Camera is disabled in config, no stream or snapshot available!</Text>

View File

@ -1,12 +1,12 @@
import React from 'react';
import { CameraConfig } from '../../types/frigateConfig';
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
import AutoUpdatingCameraImage from './frigate/AutoUpdatingCameraImage';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../router/routes.path';
import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
import { frigateApi, mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api';
import AutoUpdatedImage from './frigate/CameraImage';
import AutoUpdatedImage from './AutoUpdatedImage';
import { recordingsPageQuery } from '../../pages/RecordingsPage';
const useStyles = createStyles((theme) => ({
@ -50,7 +50,8 @@ const CameraCard = ({
navigate(url)
}
const handleOpenRecordings = () => {
throw Error('Not yet implemented')
const url = `${routesPath.RECORDINGS_PATH}?${recordingsPageQuery.hostId}=${camera.frigateHost?.id}&${recordingsPageQuery.cameraId}=${camera.id}`
navigate(url)
}
return (
<Grid.Col md={6} lg={3} p='0.2rem'>

View File

@ -7,7 +7,7 @@ import { Modal, createStyles, getStylesRef, rem, Text, Box, Flex, Grid, Divider,
import { useMediaQuery } from '@mantine/hooks';
import { dimensions } from '../dimensions/dimensions';
import { observer } from 'mobx-react-lite';
import KomponentLoader from './CogwheelLoader';
import KomponentLoader from './loaders/CogwheelLoader';
import ProductParameter from './ProductParameter';
import { productString } from '../strings/product.strings';
import { IconArrowBadgeLeft, IconArrowBadgeRight } from '@tabler/icons-react';

View 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;

View File

@ -1,4 +1,4 @@
import { Accordion, Center, Text } from '@mantine/core';
import { Accordion, Center, Loader, Text } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
import { useQuery } from '@tanstack/react-query';
@ -6,20 +6,23 @@ import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services
import DayAccordion from './DayAccordion';
import { observer } from 'mobx-react-lite';
import { Context } from '../../..';
import { getResolvedTimeZone } from '../frigate/dateUtil';
import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil';
import RetryError from '../RetryError';
import { strings } from '../../strings/strings';
import { RecordSummary } from '../../../types/record';
interface CameraAccordionProps {
camera: GetCameraWHostWConfig,
host: GetFrigateHost
}
const CameraAccordion = observer(({
const CameraAccordion = ({
camera,
host
}: CameraAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context)
const { data, isPending, isError } = useQuery({
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
queryFn: () => {
if (camera && host) {
@ -44,29 +47,46 @@ const CameraAccordion = observer(({
setOpenedDay(value)
}
if (isPending) return <Center><Text>Loading...</Text></Center>
if (isError) return <Center><Text>Loading error</Text></Center>
if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} />
if (!data || !camera) return null
const days = data.slice(0, 2).map(rec => (
<Accordion.Item key={rec.day} value={rec.day}>
<Accordion.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
<Accordion.Panel key={rec.day + 'panel'}>
<DayAccordion key={rec.day + 'day'} recordSummary={rec} />
const recodItem = (record: RecordSummary) => (
<Accordion.Item key={record.day} value={record.day}>
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control>
<Accordion.Panel key={record.day + 'panel'}>
<DayAccordion key={record.day + 'day'} recordSummary={record} />
</Accordion.Panel>
</Accordion.Item>
)
))
const days = () => {
const [startDate, endDate] = recStore.selectedRange
if (startDate && endDate) {
return data
.filter(rec => {
const parsedRecDate = parseQueryDateToDate(rec.day)
if (parsedRecDate) {
return parsedRecDate >= startDate && parsedRecDate <= endDate
}
return false
})
.map(rec => recodItem(rec))
}
if ((startDate && endDate) || (!startDate && !endDate)) {
return data.map(rec => recodItem(rec))
}
return []
}
console.log('CameraAccordion rendered')
return (
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
{days}
{days()}
</Accordion>
)
})
}
export default CameraAccordion;
export default observer(CameraAccordion);

View File

@ -1,25 +1,23 @@
import { Accordion, Center, Flex, Group, Text } from '@mantine/core';
import { Accordion, Center, Group, Text } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import { RecordHour, RecordSummary, Recording } from '../../../types/record';
import Button from '../frigate/Button';
import { IconPlayerPause, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
import { RecordSummary } from '../../../types/record';
import { observer } from 'mobx-react-lite';
import PlayControl from './PlayControl';
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { Context } from '../../..';
import VideoPlayer from '../frigate/VideoPlayer';
import { getResolvedTimeZone } from '../frigate/dateUtil';
import EventsAccordion from './EventsAccordion';
import VideoPlayer from '../players/VideoPlayer';
import { getResolvedTimeZone } from '../../utils/dateUtil';
import DayEventsAccordion from './DayEventsAccordion';
import { strings } from '../../strings/strings';
interface RecordingAccordionProps {
recordSummary?: RecordSummary
}
const DayAccordion = observer(({
const DayAccordion = ({
recordSummary
}: RecordingAccordionProps) => {
const { recordingsStore } = useContext(Context)
const { recordingsStore: recStore } = useContext(Context)
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
const [openedValue, setOpenedValue] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
@ -28,11 +26,11 @@ const DayAccordion = observer(({
if (openVideoPlayer) {
console.log('openVideoPlayer', openVideoPlayer)
if (openVideoPlayer) {
recordingsStore.recordToPlay.day = recordSummary?.day
recordingsStore.recordToPlay.hour = openVideoPlayer
recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay)
console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay)
recStore.recordToPlay.day = recordSummary?.day
recStore.recordToPlay.hour = openVideoPlayer
recStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
const parsed = recStore.getFullRecordForPlay(recStore.recordToPlay)
console.log('recordingsStore.playedRecord: ', recStore.recordToPlay)
if (parsed.success) {
const url = proxyApi.recordingURL(
parsed.data.hostName,
@ -50,10 +48,10 @@ const DayAccordion = observer(({
}
}, [openVideoPlayer])
if (!recordSummary || recordSummary.hours.length < 1) return (<Text>Not have record at that day</Text>)
if (!recordSummary ) return <Text>Not have record at that day</Text>
if (recordSummary.hours.length < 1) return <Text>Not have record at that day</Text>
const handleOpenPlayer = (hour: string) => {
// console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`)
if (openVideoPlayer !== hour) {
setOpenedValue(hour)
setOpenVideoPlayer(hour)
@ -73,6 +71,17 @@ const DayAccordion = observer(({
console.log('DayAccordion rendered')
const hourLabel = (hour: string, eventsQty: number) => (
<Group>
<Text>{strings.hour}: {hour}:00</Text>
{eventsQty > 0 ?
<Text>{strings.events}: {eventsQty}</Text>
:
<Text>{strings.notHaveEvents}</Text>
}
</Group>
)
return (
<Accordion
key={recordSummary.day}
@ -81,23 +90,27 @@ const DayAccordion = observer(({
value={openedValue}
onChange={handleClick}
>
{recordSummary.hours.slice(0, 5).map(hour => (
{recordSummary.hours.map(hour => (
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
<Accordion.Control key={hour.hour + 'Control'}>
<PlayControl label={`Hour ${hour.hour}`} value={hour.hour} openVideoPlayer={openVideoPlayer} onClick={handleOpenPlayer} />
<PlayControl
label={hourLabel(hour.hour, hour.events)}
value={hour.hour}
openVideoPlayer={openVideoPlayer}
onClick={handleOpenPlayer} />
</Accordion.Control>
<Accordion.Panel key={hour.hour + 'Panel'}>
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
{hour.events > 0 ?
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
:
<Center><Text>Not have events</Text></Center>
<Center><Text>{strings.notHaveEvents}</Text></Center>
}
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
)
})
}
export default DayAccordion;
export default observer(DayAccordion);

View File

@ -1,5 +1,6 @@
import { Accordion, Text } from '@mantine/core';
import React, { Suspense, lazy, useState } from 'react';
import { strings } from '../../strings/strings';
const EventsAccordion = lazy(() => import('./EventsAccordion'))
interface DayEventsAccordionProps {
@ -21,7 +22,7 @@ const DayEventsAccordion = ({
return (
<Accordion onChange={handleClick}>
<Accordion.Item value={hour}>
<Accordion.Control><Text>Events {qty}</Text></Accordion.Control>
<Accordion.Control><Text>{strings.events}: {qty}</Text></Accordion.Control>
<Accordion.Panel>
{openedItem === hour ?
<Suspense>

View File

@ -1,13 +1,16 @@
import { Accordion, Center, Text } from '@mantine/core';
import { Accordion, Center, Group, Loader, Text } from '@mantine/core';
import { observer } from 'mobx-react-lite';
import React, { useContext, useEffect, useState } from 'react';
import { Context } from '../../..';
import { useQuery } from '@tanstack/react-query';
import { frigateQueryKeys, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
import PlayControl from './PlayControl';
import VideoPlayer from '../frigate/VideoPlayer';
import { formatUnixTimestampToDateTime, getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../frigate/dateUtil';
import VideoPlayer from '../players/VideoPlayer';
import { getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../../utils/dateUtil';
import RetryError from '../RetryError';
import { strings } from '../../strings/strings';
import { EventFrigate } from '../../../types/event';
/**
* @param day frigate format, e.g day: 2024-02-23
@ -31,8 +34,6 @@ interface EventsAccordionProps {
const EventsAccordion = observer(({
day,
hour,
cameraName,
hostName,
// TODO labels, score
}: EventsAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context)
@ -40,17 +41,17 @@ const EventsAccordion = observer(({
const [openedValue, setOpenedValue] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
const inHostName = hostName || recStore.recordToPlay.hostName
const inCameraName = cameraName || recStore.recordToPlay.cameraName
const isRequiredParams = inCameraName && inHostName
const inHost = recStore.selectedHost
const inCamera = recStore.selectedCamera
const isRequiredParams = inHost && inCamera
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getEvents, day, hour, inCameraName, inHostName],
queryKey: [frigateQueryKeys.getEvents, inHost, inCamera, day, hour],
queryFn: () => {
if (!isRequiredParams) return null
const [startTime, endTime] = getUnixTime(day, hour)
const parsed = getEventsQuerySchema.safeParse({
hostName: inHostName,
camerasName: [inCameraName],
hostName: mapHostToHostname(inHost),
camerasName: [inCamera.name],
after: startTime,
before: endTime,
hasClip: true,
@ -78,8 +79,8 @@ const EventsAccordion = observer(({
useEffect(() => {
if (openVideoPlayer) {
console.log('openVideoPlayer', openVideoPlayer)
if (openVideoPlayer && inHostName) {
const url = proxyApi.eventURL(inHostName, openVideoPlayer)
if (openVideoPlayer && inHost) {
const url = proxyApi.eventURL(mapHostToHostname(inHost), openVideoPlayer)
console.log('GET EVENT URL: ', url)
setPlayerUrl(url)
}
@ -88,8 +89,8 @@ const EventsAccordion = observer(({
}
}, [openVideoPlayer])
if (isPending) return <Center><Text>Loading...</Text></Center>
if (isError) return <Center><Text>Loading error</Text></Center>
if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} />
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
const handleOpenPlayer = (eventId: string) => {
@ -111,6 +112,20 @@ const EventsAccordion = observer(({
setOpenVideoPlayer(undefined)
}
const eventLabel = (event: EventFrigate) => {
const time = unixTimeToDate(event.start_time)
const duration = getDurationFromTimestamps(event.start_time, event.end_time)
return (
<Group>
<Text>{strings.player.object}: {event.label}</Text>
<Text>{time}</Text>
{duration ?
<Text>{duration}</Text>
: <></>}
</Group>
)
}
return (
<Accordion
variant='separated'
@ -118,21 +133,25 @@ const EventsAccordion = observer(({
value={openedValue}
onChange={handleClick}
>
{data.slice(0, 5).map(event => (
{data.map(event => (
<Accordion.Item key={event.id + 'Item'} value={event.id}>
<Accordion.Control key={event.id + 'Control'}>
<PlayControl
label={unixTimeToDate(event.start_time)}
label={eventLabel(event)}
value={event.id}
openVideoPlayer={openVideoPlayer}
onClick={handleOpenPlayer} />
</Accordion.Control>
<Accordion.Panel key={event.id + 'Panel'}>
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
<Text>Camera: {event.camera}</Text>
<Text>Label: {event.label}</Text>
<Text>Start: {unixTimeToDate(event.start_time)}</Text>
<Text>Duration: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
<Group mt='1rem'>
<Text>{strings.camera}: {event.camera}</Text>
<Text>{strings.player.object}: {event.label}</Text>
</Group>
<Group>
<Text>{strings.player.startTime}: {unixTimeToDate(event.start_time)}</Text>
<Text>{strings.player.duration}: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
</Group>
</Accordion.Panel>
</Accordion.Item>
))}

View File

@ -1,9 +1,33 @@
import { Flex, Group, Text } from '@mantine/core';
import { IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
import React from 'react';
import { Flex, Group, Text, createStyles } from '@mantine/core';
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
import { strings } from '../../strings/strings';
const useStyles = createStyles((theme) => ({
group: {
backgroundColor: theme.colors.blue[7],
borderRadius: '1rem',
paddingLeft: '0.5rem',
paddingRight: '0.3rem',
'&:hover': {
backgroundColor: theme.fn.darken(theme.colors.blue[7], 0.2),
},
},
text: {
color: theme.white,
fontWeight: 'normal'
},
iconStop: {
color: theme.colors.red[5]
},
iconPlay: {
color: theme.colors.green[5]
}
}))
interface PlayControlProps {
label: string,
label: string | JSX.Element,
value: string,
openVideoPlayer?: string,
onClick?: (value: string) => void
@ -15,26 +39,38 @@ const PlayControl = ({
openVideoPlayer,
onClick
}: PlayControlProps) => {
const { classes } = useStyles();
const handleClick = (value: string) => {
if (onClick) onClick(value)
}
return (
<Flex justify='space-between'>
{label}
<Group>
<Text onClick={(event) => {
<Group className={classes.group}
onClick={(event) => {
event.stopPropagation()
handleClick(value)
}}
>
<Text className={classes.text}
onClick={(event) => {
event.stopPropagation()
handleClick(value)
}}>
{openVideoPlayer === value ? 'Stop Video' : 'Play Video'}
{openVideoPlayer === value ? strings.player.stopVideo : strings.player.startVideo}
</Text>
{openVideoPlayer === value ?
<IconPlayerStop onClick={(event) => {
<IconPlayerStopFilled
className={classes.iconStop}
onClick={(event) => {
event.stopPropagation()
handleClick(value)
}} />
:
<IconPlayerPlay onClick={(event) => {
<IconPlayerPlayFilled
className={classes.iconPlay}
onClick={(event) => {
event.stopPropagation()
handleClick(value)
}} />

View File

@ -1,11 +0,0 @@
import React from 'react';
const TestAccordion = ({ camera }: { camera: any }) => {
return (
<div>
TEST ACCORDION {camera.name}
</div>
);
};
export default TestAccordion;

View File

@ -3,9 +3,11 @@ import React, { useContext, useEffect } from 'react';
import { Context } from '../../..';
import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import CogwheelLoader from '../CogwheelLoader';
import { Center, Text } from '@mantine/core';
import CogwheelLoader from '../loaders/CogwheelLoader';
import { Center, Loader, Text } from '@mantine/core';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
import { strings } from '../../strings/strings';
import RetryError from '../RetryError';
interface CameraSelectFilterProps {
selectedHostId: string,
@ -16,7 +18,7 @@ const CameraSelectFilter = ({
}: CameraSelectFilterProps) => {
const { recordingsStore: recStore } = useContext(Context)
const { data, isError, isPending, isSuccess } = useQuery({
const { data, isError, isPending, isSuccess, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId],
queryFn: () => frigateApi.getHost(selectedHostId)
})
@ -30,8 +32,8 @@ const CameraSelectFilter = ({
}
}, [isSuccess])
if (isPending) return <CogwheelLoader />
if (isError) return <Center><Text>Loading error!</Text></Center>
if (isPending) return <Loader />
if (isError) return <RetryError onRetry={refetch}/>
if (!data) return null
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
@ -51,7 +53,7 @@ const CameraSelectFilter = ({
return (
<OneSelectFilter
id='frigate-cameras'
label='Select camera:'
label={strings.selectCamera}
spaceBetween='1rem'
value={recStore.selectedCamera?.id || ''}
defaultValue={recStore.selectedCamera?.id || ''}

View 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);

View 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);

View File

@ -1,97 +1,99 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import CameraImage from "./CameraImage";
import { CameraConfig } from "../../../types/frigateConfig";
import { useDocumentVisibility } from "@mantine/hooks";
import { AspectRatio, Flex } from "@mantine/core";
// import { useCallback, useEffect, useMemo, useState } from "react";
// import CameraImage from "./CameraImage";
// import { CameraConfig } from "../../../types/frigateConfig";
// import { useDocumentVisibility } from "@mantine/hooks";
// import { AspectRatio, Flex } from "@mantine/core";
interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
cameraConfig?: CameraConfig
searchParams?: {};
showFps?: boolean;
className?: string;
url: string
};
export {}
// TODO Delete
export default function AutoUpdatingCameraImage({
cameraConfig,
searchParams = "",
showFps = true,
className,
url,
...rest
}: AutoUpdatingCameraImageProps) {
const [key, setKey] = useState(Date.now());
const [fps, setFps] = useState<string>("0");
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
// interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
// cameraConfig?: CameraConfig
// searchParams?: {};
// showFps?: boolean;
// className?: string;
// url: string
// };
const windowVisible = useDocumentVisibility()
// // TODO Delete
// export default function AutoUpdatingCameraImage({
// cameraConfig,
// searchParams = "",
// showFps = true,
// className,
// url,
// ...rest
// }: AutoUpdatingCameraImageProps) {
// const [key, setKey] = useState(Date.now());
// const [fps, setFps] = useState<string>("0");
// const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
// const windowVisible = useDocumentVisibility()
const reloadInterval = useMemo(() => {
if (windowVisible === "hidden") {
return -1; // no reason to update the image when the window is not visible
}
// const reloadInterval = useMemo(() => {
// if (windowVisible === "hidden") {
// return -1; // no reason to update the image when the window is not visible
// }
// if (liveReady) {
// return 60000;
// }
// // if (liveReady) {
// // return 60000;
// // }
// if (cameraActive) {
// return 200;
// }
// // if (cameraActive) {
// // return 200;
// // }
return 30000;
}, [windowVisible]);
// return 30000;
// }, [windowVisible]);
useEffect(() => {
if (reloadInterval == -1) {
return;
}
// useEffect(() => {
// if (reloadInterval == -1) {
// return;
// }
setKey(Date.now());
// setKey(Date.now());
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(undefined);
}
};
}, [reloadInterval]);
// return () => {
// if (timeoutId) {
// clearTimeout(timeoutId);
// setTimeoutId(undefined);
// }
// };
// }, [reloadInterval]);
const handleLoad = useCallback(() => {
if (reloadInterval == -1) {
return;
}
// const handleLoad = useCallback(() => {
// if (reloadInterval == -1) {
// return;
// }
const loadTime = Date.now() - key;
// const loadTime = Date.now() - key;
if (showFps) {
setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
}
// if (showFps) {
// setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
// }
setTimeoutId(
setTimeout(
() => {
setKey(Date.now());
},
loadTime > reloadInterval ? 1 : reloadInterval
)
);
}, [key, setFps]);
// setTimeoutId(
// setTimeout(
// () => {
// setKey(Date.now());
// },
// loadTime > reloadInterval ? 1 : reloadInterval
// )
// );
// }, [key, setFps]);
return (
// <AspectRatio ratio={1}>
<Flex direction='column' h='100%'>
{/* <CameraImage
cameraConfig={cameraConfig}
onload={handleLoad}
enabled={cameraConfig?.enabled}
url={url}
{...rest}
/> */}
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
</Flex>
// </AspectRatio >
);
}
// return (
// // <AspectRatio ratio={1}>
// <Flex direction='column' h='100%'>
// {/* <CameraImage
// cameraConfig={cameraConfig}
// onload={handleLoad}
// enabled={cameraConfig?.enabled}
// url={url}
// {...rest}
// /> */}
// {showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
// </Flex>
// // </AspectRatio >
// );
// }

View File

@ -1,115 +1,117 @@
import Tooltip from './Tooltip';
import { Fragment, useCallback, useRef, useState } from 'react';
// import Tooltip from './Tooltip';
// import { Fragment, useCallback, useRef, useState } from 'react';
const ButtonColors = {
blue: {
contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300',
outlined:
'text-blue-500 border-2 border-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
text: 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
iconOnly: 'text-blue-500 hover:text-blue-200',
},
red: {
contained: 'bg-red-500 focus:bg-red-400 active:bg-red-600 ring-red-300',
outlined:
'text-red-500 border-2 border-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
text: 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
iconOnly: 'text-red-500 hover:text-red-200',
},
yellow: {
contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
outlined:
'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
text: 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
iconOnly: 'text-yellow-500 hover:text-yellow-200',
},
green: {
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
outlined:
'text-green-500 border-2 border-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
text: 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
iconOnly: 'text-green-500 hover:text-green-200',
},
gray: {
contained: 'bg-gray-500 focus:bg-gray-400 active:bg-gray-600 ring-gray-300',
outlined:
'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
iconOnly: 'text-gray-500 hover:text-gray-200',
},
disabled: {
contained: 'bg-gray-400',
outlined:
'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
iconOnly: 'text-gray-500 hover:text-gray-200',
},
black: {
contained: '',
outlined: '',
text: 'text-black dark:text-white',
iconOnly: '',
},
};
export {}
const ButtonTypes = {
contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
outlined: '',
text: 'transition-opacity',
iconOnly: 'transition-opacity',
};
// const ButtonColors = {
// blue: {
// contained: 'bg-blue-500 focus:bg-blue-400 active:bg-blue-600 ring-blue-300',
// outlined:
// 'text-blue-500 border-2 border-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
// text: 'text-blue-500 hover:bg-blue-500 hover:bg-opacity-20 focus:bg-blue-500 focus:bg-opacity-40 active:bg-blue-500 active:bg-opacity-40',
// iconOnly: 'text-blue-500 hover:text-blue-200',
// },
// red: {
// contained: 'bg-red-500 focus:bg-red-400 active:bg-red-600 ring-red-300',
// outlined:
// 'text-red-500 border-2 border-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
// text: 'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
// iconOnly: 'text-red-500 hover:text-red-200',
// },
// yellow: {
// contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
// outlined:
// 'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
// text: 'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
// iconOnly: 'text-yellow-500 hover:text-yellow-200',
// },
// green: {
// contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
// outlined:
// 'text-green-500 border-2 border-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
// text: 'text-green-500 hover:bg-green-500 hover:bg-opacity-20 focus:bg-green-500 focus:bg-opacity-40 active:bg-green-500 active:bg-opacity-40',
// iconOnly: 'text-green-500 hover:text-green-200',
// },
// gray: {
// contained: 'bg-gray-500 focus:bg-gray-400 active:bg-gray-600 ring-gray-300',
// outlined:
// 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
// text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
// iconOnly: 'text-gray-500 hover:text-gray-200',
// },
// disabled: {
// contained: 'bg-gray-400',
// outlined:
// 'text-gray-500 border-2 border-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
// text: 'text-gray-500 hover:bg-gray-500 hover:bg-opacity-20 focus:bg-gray-500 focus:bg-opacity-40 active:bg-gray-500 active:bg-opacity-40',
// iconOnly: 'text-gray-500 hover:text-gray-200',
// },
// black: {
// contained: '',
// outlined: '',
// text: 'text-black dark:text-white',
// iconOnly: '',
// },
// };
export default function Button({
children,
className = '',
color = 'blue',
disabled = false,
ariaCapitalize = false,
href,
target,
type = 'contained',
...attrs
}) {
const [hovered, setHovered] = useState(false);
const ref = useRef();
// const ButtonTypes = {
// contained: 'text-white shadow focus:shadow-xl hover:shadow-md',
// outlined: '',
// text: 'transition-opacity',
// iconOnly: 'transition-opacity',
// };
let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
ButtonColors[disabled ? 'disabled' : color][type]
} font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer`
}`;
// export default function Button({
// children,
// className = '',
// color = 'blue',
// disabled = false,
// ariaCapitalize = false,
// href,
// target,
// type = 'contained',
// ...attrs
// }) {
// const [hovered, setHovered] = useState(false);
// const ref = useRef();
if (disabled) {
classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
}
// let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
// ButtonColors[disabled ? 'disabled' : color][type]
// } font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
// disabled ? 'cursor-not-allowed' : `${type == 'iconOnly' ? '' : 'focus:ring-2'} cursor-pointer`
// }`;
const handleMousenter = useCallback(() => {
setHovered(true);
}, []);
// if (disabled) {
// classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
// }
const handleMouseleave = useCallback(() => {
setHovered(false);
}, []);
// const handleMousenter = useCallback(() => {
// setHovered(true);
// }, []);
const Element = href ? 'a' : 'div';
// const handleMouseleave = useCallback(() => {
// setHovered(false);
// }, []);
return (
<Fragment>
<Element
role="button"
aria-disabled={disabled ? 'true' : 'false'}
tabindex="0"
className={classes}
href={href}
target={target}
ref={ref}
onmouseenter={handleMousenter}
onmouseleave={handleMouseleave}
{...attrs}
>
{children}
</Element>
{hovered && attrs['aria-label'] ? <Tooltip text={attrs['aria-label']} relativeTo={ref} capitalize={ariaCapitalize} /> : null}
</Fragment>
);
}
// const Element = href ? 'a' : 'div';
// return (
// <Fragment>
// <Element
// role="button"
// aria-disabled={disabled ? 'true' : 'false'}
// tabindex="0"
// className={classes}
// href={href}
// target={target}
// ref={ref}
// onmouseenter={handleMousenter}
// onmouseleave={handleMouseleave}
// {...attrs}
// >
// {children}
// </Element>
// {hovered && attrs['aria-label'] ? <Tooltip text={attrs['aria-label']} relativeTo={ref} capitalize={ariaCapitalize} /> : null}
// </Fragment>
// );
// }

View File

@ -1,159 +1,161 @@
import { useCallback, useMemo, useState } from "react";
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
import { CameraConfig } from "../../../types/frigateConfig";
import { usePersistence } from "../../../hooks/use-persistence";
import { Button, Switch, Text } from "@mantine/core";
import { Card, CardContent, CardHeader, CardTitle } from "./card";
import { IconSettings } from "@tabler/icons-react";
// import { useCallback, useMemo, useState } from "react";
// import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
// import { CameraConfig } from "../../../types/frigateConfig";
// import { usePersistence } from "../../../hooks/use-persistence";
// import { Button, Switch, Text } from "@mantine/core";
// import { Card, CardContent, CardHeader, CardTitle } from "./card";
// import { IconSettings } from "@tabler/icons-react";
type Options = { [key: string]: boolean };
export {}
const emptyObject = Object.freeze({});
// type Options = { [key: string]: boolean };
type DebugCameraImageProps = {
className?: string;
cameraConfig: CameraConfig
url: string
};
// const emptyObject = Object.freeze({});
export default function DebugCameraImage({
className,
cameraConfig,
url,
}: DebugCameraImageProps) {
const [showSettings, setShowSettings] = useState(false);
const [options, setOptions] = usePersistence(
`${cameraConfig?.name}-feed`,
emptyObject
);
const handleSetOption = useCallback(
(id: string, value: boolean) => {
const newOptions = { ...options, [id]: value };
setOptions(newOptions);
},
[options]
);
const searchParams = useMemo(
() =>
new URLSearchParams(
Object.keys(options).reduce((memo, key) => {
//@ts-ignore we know this is correct
memo.push([key, options[key] === true ? "1" : "0"]);
return memo;
}, [])
),
[options]
);
const handleToggleSettings = useCallback(() => {
setShowSettings(!showSettings);
}, [showSettings]);
// type DebugCameraImageProps = {
// className?: string;
// cameraConfig: CameraConfig
// url: string
// };
return (
<div className={className}>
<AutoUpdatingCameraImage
cameraConfig={cameraConfig}
searchParams={searchParams}
url={url}
/>
<Button onClick={handleToggleSettings} variant="link" size="sm">
<span className="w-5 h-5">
<IconSettings />
</span>{" "}
<span>{showSettings ? "Hide" : "Show"} Options</span>
</Button>
{showSettings ? (
<Card>
<CardHeader>
<CardTitle>Options</CardTitle>
</CardHeader>
<CardContent>
<DebugSettings
handleSetOption={handleSetOption}
options={options}
/>
</CardContent>
</Card>
) : null}
</div>
);
}
// export default function DebugCameraImage({
// className,
// cameraConfig,
// url,
// }: DebugCameraImageProps) {
// const [showSettings, setShowSettings] = useState(false);
// const [options, setOptions] = usePersistence(
// `${cameraConfig?.name}-feed`,
// emptyObject
// );
// const handleSetOption = useCallback(
// (id: string, value: boolean) => {
// const newOptions = { ...options, [id]: value };
// setOptions(newOptions);
// },
// [options]
// );
// const searchParams = useMemo(
// () =>
// new URLSearchParams(
// Object.keys(options).reduce((memo, key) => {
// //@ts-ignore we know this is correct
// memo.push([key, options[key] === true ? "1" : "0"]);
// return memo;
// }, [])
// ),
// [options]
// );
// const handleToggleSettings = useCallback(() => {
// setShowSettings(!showSettings);
// }, [showSettings]);
type DebugSettingsProps = {
handleSetOption: (id: string, value: boolean) => void;
options: Options;
};
// return (
// <div className={className}>
// <AutoUpdatingCameraImage
// cameraConfig={cameraConfig}
// searchParams={searchParams}
// url={url}
// />
// <Button onClick={handleToggleSettings} variant="link" size="sm">
// <span className="w-5 h-5">
// <IconSettings />
// </span>{" "}
// <span>{showSettings ? "Hide" : "Show"} Options</span>
// </Button>
// {showSettings ? (
// <Card>
// <CardHeader>
// <CardTitle>Options</CardTitle>
// </CardHeader>
// <CardContent>
// <DebugSettings
// handleSetOption={handleSetOption}
// options={options}
// />
// </CardContent>
// </Card>
// ) : null}
// </div>
// );
// }
function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="flex items-center space-x-2">
<Switch
id="bbox"
checked={options["bbox"]}
onChange={() => { }}
// onCheckedChange={(isChecked) => {
// handleSetOption("bbox", isChecked);
// }}
/>
{/* <Label htmlFor="bbox">Bounding Box</Label> */}
<Text>Bounding Box</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="timestamp"
checked={options["timestamp"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("timestamp", isChecked);
// }}
/>
{/* <Label htmlFor="timestamp">Timestamp</Label> */}
<Text>Timestamp</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="zones"
checked={options["zones"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("zones", isChecked);
// }}
/>
{/* <Label htmlFor="zones">Zones</Label> */}
<Text>Zones</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="mask"
checked={options["mask"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("mask", isChecked);
// }}
/>
{/* <Label htmlFor="mask">Mask</Label> */}
<Text>Mask</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="motion"
checked={options["motion"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("motion", isChecked);
// }}
/>
{/* <Label htmlFor="motion">Motion</Label> */}
<Text>Motion</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="regions"
checked={options["regions"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("regions", isChecked);
// }}
/>
{/* <Label htmlFor="regions">Regions</Label> */}
<Text>Regions</Text>
</div>
</div>
);
}
// type DebugSettingsProps = {
// handleSetOption: (id: string, value: boolean) => void;
// options: Options;
// };
// function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
// return (
// <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
// <div className="flex items-center space-x-2">
// <Switch
// id="bbox"
// checked={options["bbox"]}
// onChange={() => { }}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("bbox", isChecked);
// // }}
// />
// {/* <Label htmlFor="bbox">Bounding Box</Label> */}
// <Text>Bounding Box</Text>
// </div>
// <div className="flex items-center space-x-2">
// <Switch
// id="timestamp"
// checked={options["timestamp"]}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("timestamp", isChecked);
// // }}
// />
// {/* <Label htmlFor="timestamp">Timestamp</Label> */}
// <Text>Timestamp</Text>
// </div>
// <div className="flex items-center space-x-2">
// <Switch
// id="zones"
// checked={options["zones"]}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("zones", isChecked);
// // }}
// />
// {/* <Label htmlFor="zones">Zones</Label> */}
// <Text>Zones</Text>
// </div>
// <div className="flex items-center space-x-2">
// <Switch
// id="mask"
// checked={options["mask"]}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("mask", isChecked);
// // }}
// />
// {/* <Label htmlFor="mask">Mask</Label> */}
// <Text>Mask</Text>
// </div>
// <div className="flex items-center space-x-2">
// <Switch
// id="motion"
// checked={options["motion"]}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("motion", isChecked);
// // }}
// />
// {/* <Label htmlFor="motion">Motion</Label> */}
// <Text>Motion</Text>
// </div>
// <div className="flex items-center space-x-2">
// <Switch
// id="regions"
// checked={options["regions"]}
// // onCheckedChange={(isChecked) => {
// // handleSetOption("regions", isChecked);
// // }}
// />
// {/* <Label htmlFor="regions">Regions</Label> */}
// <Text>Regions</Text>
// </div>
// </div>
// );
// }

View File

@ -1,35 +1,37 @@
import { h, Fragment } from 'preact';
import { createPortal } from 'preact/compat';
import { useState, useEffect } from 'preact/hooks';
// import { h, Fragment } from 'preact';
// import { createPortal } from 'preact/compat';
// import { useState, useEffect } from 'preact/hooks';
export default function Dialog({ children, portalRootID = 'dialogs' }) {
const portalRoot = portalRootID && document.getElementById(portalRootID);
const [show, setShow] = useState(false);
export {}
useEffect(() => {
window.requestAnimationFrame(() => {
setShow(true);
});
}, []);
// export default function Dialog({ children, portalRootID = 'dialogs' }) {
// const portalRoot = portalRootID && document.getElementById(portalRootID);
// const [show, setShow] = useState(false);
const dialog = (
<Fragment>
<div
data-testid="scrim"
key="scrim"
className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
>
<div
role="modal"
className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 sm:max-w-sm md:max-w-md lg:max-w-lg text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
show ? 'scale-100 opacity-100' : ''
}`}
>
{children}
</div>
</div>
</Fragment>
);
// useEffect(() => {
// window.requestAnimationFrame(() => {
// setShow(true);
// });
// }, []);
return portalRoot ? createPortal(dialog, portalRoot) : dialog;
}
// const dialog = (
// <Fragment>
// <div
// data-testid="scrim"
// key="scrim"
// className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
// >
// <div
// role="modal"
// className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 sm:max-w-sm md:max-w-md lg:max-w-lg text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
// show ? 'scale-100 opacity-100' : ''
// }`}
// >
// {children}
// </div>
// </div>
// </Fragment>
// );
// return portalRoot ? createPortal(dialog, portalRoot) : dialog;
// }

View File

@ -1,16 +1,18 @@
import { h } from 'preact';
import { Link as RouterLink } from 'preact-router/match';
// import { h } from 'preact';
// import { Link as RouterLink } from 'preact-router/match';
export default function Link({
activeClassName = '',
className = 'text-blue-500 hover:underline',
children,
href,
...props
}) {
return (
<RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
{children}
</RouterLink>
);
}
export {}
// export default function Link({
// activeClassName = '',
// className = 'text-blue-500 hover:underline',
// children,
// href,
// ...props
// }) {
// return (
// <RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
// {children}
// </RouterLink>
// );
// }

View File

@ -1,48 +1,50 @@
import { h } from 'preact';
import RelativeModal from './RelativeModal';
import { useCallback } from 'preact/hooks';
// import { h } from 'preact';
// import RelativeModal from './RelativeModal';
// import { useCallback } from 'preact/hooks';
export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
return relativeTo ? (
<RelativeModal
children={children}
className={`${className || ''} py-2`}
role="listbox"
onDismiss={onDismiss}
portalRootID="menus"
relativeTo={relativeTo}
widthRelative={widthRelative}
/>
) : null;
}
export {}
export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
const handleClick = useCallback(() => {
onSelect && onSelect(value, label);
}, [onSelect, value, label]);
// export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
// return relativeTo ? (
// <RelativeModal
// children={children}
// className={`${className || ''} py-2`}
// role="listbox"
// onDismiss={onDismiss}
// portalRootID="menus"
// relativeTo={relativeTo}
// widthRelative={widthRelative}
// />
// ) : null;
// }
const Element = href ? 'a' : 'div';
// export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
// const handleClick = useCallback(() => {
// onSelect && onSelect(value, label);
// }, [onSelect, value, label]);
return (
<Element
className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${
focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : ''
}`}
href={href}
onClick={handleClick}
role="option"
{...attrs}
>
{Icon ? (
<div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
<Icon />
</div>
) : null}
<div className="whitespace-nowrap">{label}</div>
</Element>
);
}
// const Element = href ? 'a' : 'div';
export function MenuSeparator() {
return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
}
// return (
// <Element
// className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${
// focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : ''
// }`}
// href={href}
// onClick={handleClick}
// role="option"
// {...attrs}
// >
// {Icon ? (
// <div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
// <Icon />
// </div>
// ) : null}
// <div className="whitespace-nowrap">{label}</div>
// </Element>
// );
// }
// export function MenuSeparator() {
// return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
// }

View File

@ -1,70 +1,72 @@
import { h } from 'preact';
import { useRef, useState } from 'preact/hooks';
import Menu from './Menu';
import { ArrowDropdown } from '../icons/ArrowDropdown';
import Heading from './Heading';
import Button from './Button';
import SelectOnlyIcon from '../icons/SelectOnly';
// import { h } from 'preact';
// import { useRef, useState } from 'preact/hooks';
// import Menu from './Menu';
// import { ArrowDropdown } from '../icons/ArrowDropdown';
// import Heading from './Heading';
// import Button from './Button';
// import SelectOnlyIcon from '../icons/SelectOnly';
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
const popupRef = useRef(null);
export {}
const [state, setState] = useState({
showMenu: false,
});
// export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
// const popupRef = useRef(null);
const isOptionSelected = (item) => {
return selection == 'all' || selection.split(',').indexOf(item) > -1;
};
// const [state, setState] = useState({
// showMenu: false,
// });
const menuHeight = Math.round(window.innerHeight * 0.55);
return (
<div className={`${className} p-2`} ref={popupRef}>
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
<label>{title}</label>
<ArrowDropdown className="w-6" />
</div>
{state.showMenu ? (
<Menu
className={`max-h-[${menuHeight}px] overflow-auto`}
relativeTo={popupRef}
onDismiss={() => setState({ showMenu: false })}
>
<div className="flex flex-wrap justify-between items-center">
<Heading className="p-4 justify-center" size="md">
{title}
</Heading>
<Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
Show All
</Button>
</div>
{options.map((item) => (
<div className="flex flex-grow" key={item}>
<label
className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
>
<input
className="mx-4 m-0 align-middle"
type="checkbox"
checked={isOptionSelected(item)}
onChange={() => onToggle(item)}
/>
{item.replaceAll('_', ' ')}
</label>
<div className="justify-right">
<Button
color={isOptionSelected(item) ? 'blue' : 'black'}
type="text"
className="max-h-[35px] mx-2"
onClick={() => onSelectSingle(item)}
>
{ ( <SelectOnlyIcon /> ) }
</Button>
</div>
</div>
))}
</Menu>
) : null}
</div>
);
}
// const isOptionSelected = (item) => {
// return selection == 'all' || selection.split(',').indexOf(item) > -1;
// };
// const menuHeight = Math.round(window.innerHeight * 0.55);
// return (
// <div className={`${className} p-2`} ref={popupRef}>
// <div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
// <label>{title}</label>
// <ArrowDropdown className="w-6" />
// </div>
// {state.showMenu ? (
// <Menu
// className={`max-h-[${menuHeight}px] overflow-auto`}
// relativeTo={popupRef}
// onDismiss={() => setState({ showMenu: false })}
// >
// <div className="flex flex-wrap justify-between items-center">
// <Heading className="p-4 justify-center" size="md">
// {title}
// </Heading>
// <Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
// Show All
// </Button>
// </div>
// {options.map((item) => (
// <div className="flex flex-grow" key={item}>
// <label
// className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
// >
// <input
// className="mx-4 m-0 align-middle"
// type="checkbox"
// checked={isOptionSelected(item)}
// onChange={() => onToggle(item)}
// />
// {item.replaceAll('_', ' ')}
// </label>
// <div className="justify-right">
// <Button
// color={isOptionSelected(item) ? 'blue' : 'black'}
// type="text"
// className="max-h-[35px] mx-2"
// onClick={() => onSelectSingle(item)}
// >
// { ( <SelectOnlyIcon /> ) }
// </Button>
// </div>
// </div>
// ))}
// </Menu>
// ) : null}
// </div>
// );
// }

View File

@ -1,41 +1,43 @@
import { h } from 'preact';
import { useCallback, useState } from 'preact/hooks';
// import { h } from 'preact';
// import { useCallback, useState } from 'preact/hooks';
export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
export {}
const handleSelected = useCallback(
(index) => () => {
setSelectedIndex(index);
onChange && onChange(index);
},
[onChange]
);
// export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, className }) {
// const [selectedIndex, setSelectedIndex] = useState(selectedIndexProp);
const RenderChildren = useCallback(() => {
return children.map((child, i) => {
child.props.selected = i === selectedIndex;
child.props.onClick = handleSelected(i);
return child;
});
}, [selectedIndex, children, handleSelected]);
// const handleSelected = useCallback(
// (index) => () => {
// setSelectedIndex(index);
// onChange && onChange(index);
// },
// [onChange]
// );
return (
<div className={`flex ${className}`}>
<RenderChildren />
</div>
);
}
// const RenderChildren = useCallback(() => {
// return children.map((child, i) => {
// child.props.selected = i === selectedIndex;
// child.props.onClick = handleSelected(i);
// return child;
// });
// }, [selectedIndex, children, handleSelected]);
export function TextTab({ selected, text, onClick, disabled }) {
const selectedStyle = disabled
? 'text-gray-400 dark:text-gray-600 bg-transparent'
: selected
? 'text-white bg-blue-500 dark:text-black dark:bg-white'
: 'text-black dark:text-white bg-transparent';
return (
<button onClick={onClick} disabled={disabled} className={`rounded-full px-4 py-2 ${selectedStyle}`}>
<span>{text}</span>
</button>
);
}
// return (
// <div className={`flex ${className}`}>
// <RenderChildren />
// </div>
// );
// }
// export function TextTab({ selected, text, onClick, disabled }) {
// const selectedStyle = disabled
// ? 'text-gray-400 dark:text-gray-600 bg-transparent'
// : selected
// ? 'text-white bg-blue-500 dark:text-black dark:bg-white'
// : 'text-black dark:text-white bg-transparent';
// return (
// <button onClick={onClick} disabled={disabled} className={`rounded-full px-4 py-2 ${selectedStyle}`}>
// <span>{text}</span>
// </button>
// );
// }

View File

@ -1,83 +1,85 @@
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
// import { FunctionComponent, useEffect, useMemo, useState } from 'react';
interface IProp {
/** The time to calculate time-ago from */
time: Date;
/** OPTIONAL: overwrite current time */
currentTime?: Date;
/** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
dense?: boolean;
/** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
refreshInterval?: number;
}
// interface IProp {
// /** The time to calculate time-ago from */
// time: Date;
// /** OPTIONAL: overwrite current time */
// currentTime?: Date;
// /** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
// dense?: boolean;
// /** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
// refreshInterval?: number;
// }
type TimeUnit = {
unit: string;
full: string;
value: number;
};
export {}
const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
// type TimeUnit = {
// unit: string;
// full: string;
// value: number;
// };
const pastTime: Date = new Date(time);
const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
// const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
// if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
const timeUnits: TimeUnit[] = [
{ unit: 'yr', full: 'year', value: 31536000 },
{ unit: 'mo', full: 'month', value: 0 },
{ unit: 'd', full: 'day', value: 86400 },
{ unit: 'h', full: 'hour', value: 3600 },
{ unit: 'm', full: 'minute', value: 60 },
{ unit: 's', full: 'second', value: 1 },
];
// const pastTime: Date = new Date(time);
// const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
const elapsed: number = elapsedTime / 1000;
if (elapsed < 10) {
return 'just now';
}
// const timeUnits: TimeUnit[] = [
// { unit: 'yr', full: 'year', value: 31536000 },
// { unit: 'mo', full: 'month', value: 0 },
// { unit: 'd', full: 'day', value: 86400 },
// { unit: 'h', full: 'hour', value: 3600 },
// { unit: 'm', full: 'minute', value: 60 },
// { unit: 's', full: 'second', value: 1 },
// ];
for (let i = 0; i < timeUnits.length; i++) {
// if months
if (i === 1) {
// Get the month and year for the time provided
const pastMonth = pastTime.getUTCMonth();
const pastYear = pastTime.getUTCFullYear();
// const elapsed: number = elapsedTime / 1000;
// if (elapsed < 10) {
// return 'just now';
// }
// get current month and year
const currentMonth = currentTime.getUTCMonth();
const currentYear = currentTime.getUTCFullYear();
// for (let i = 0; i < timeUnits.length; i++) {
// // if months
// if (i === 1) {
// // Get the month and year for the time provided
// const pastMonth = pastTime.getUTCMonth();
// const pastYear = pastTime.getUTCFullYear();
let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
// // get current month and year
// const currentMonth = currentTime.getUTCMonth();
// const currentYear = currentTime.getUTCFullYear();
// check if the time provided is the previous month but not exceeded 1 month ago.
if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
monthDiff--;
}
// let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
if (monthDiff > 0) {
const unitAmount = monthDiff;
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
}
} else if (elapsed >= timeUnits[i].value) {
const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
}
}
return 'Invalid Time';
};
// // check if the time provided is the previous month but not exceeded 1 month ago.
// if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
// monthDiff--;
// }
const TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
const [currentTime, setCurrentTime] = useState<Date>(new Date());
useEffect(() => {
const intervalId: NodeJS.Timeout = setInterval(() => {
setCurrentTime(new Date());
}, refreshInterval);
return () => clearInterval(intervalId);
}, [refreshInterval]);
// if (monthDiff > 0) {
// const unitAmount = monthDiff;
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
// }
// } else if (elapsed >= timeUnits[i].value) {
// const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
// return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
// }
// }
// return 'Invalid Time';
// };
const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
// const TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
// const [currentTime, setCurrentTime] = useState<Date>(new Date());
// useEffect(() => {
// const intervalId: NodeJS.Timeout = setInterval(() => {
// setCurrentTime(new Date());
// }, refreshInterval);
// return () => clearInterval(intervalId);
// }, [refreshInterval]);
return <span>{timeAgoValue}</span>;
};
export default TimeAgo;
// const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
// return <span>{timeAgoValue}</span>;
// };
// export default TimeAgo;

View File

@ -1,65 +1,67 @@
import { Fragment, h } from 'preact';
import { useState } from 'preact/hooks';
// import { Fragment, h } from 'preact';
// import { useState } from 'preact/hooks';
export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100);
const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100);
const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100);
const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100);
export {}
const [isHovering, setIsHovering] = useState(false);
const getHoverStyle = () => {
if (boxLeftEdge < 15) {
// show object stats on right side
return {
left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
top: `${boxTopEdge}%`,
};
}
// export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) {
// const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100);
// const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100);
// const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100);
// const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100);
return {
right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
top: `${boxTopEdge}%`,
};
};
// const [isHovering, setIsHovering] = useState(false);
// const getHoverStyle = () => {
// if (boxLeftEdge < 15) {
// // show object stats on right side
// return {
// left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`,
// top: `${boxTopEdge}%`,
// };
// }
const getObjectArea = () => {
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
return Math.round(width * height);
};
// return {
// right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`,
// top: `${boxTopEdge}%`,
// };
// };
const getObjectRatio = () => {
const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
return Math.round(100 * (width / height)) / 100;
};
// const getObjectArea = () => {
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
// return Math.round(width * height);
// };
return (
<Fragment>
<div
className="absolute border-4 border-red-600"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onTouchStart={() => setIsHovering(true)}
onTouchEnd={() => setIsHovering(false)}
style={{
left: `${boxLeftEdge}%`,
top: `${boxTopEdge}%`,
right: `${boxRightEdge}%`,
bottom: `${boxBottomEdge}%`,
}}
>
{eventOverlay.class_type == 'entered_zone' ? (
<div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
) : null}
</div>
{isHovering && (
<div className="absolute bg-white dark:bg-slate-800 p-4 block text-black dark:text-white text-lg" style={getHoverStyle()}>
<div>{`Area: ${getObjectArea()} px`}</div>
<div>{`Ratio: ${getObjectRatio()}`}</div>
</div>
)}
</Fragment>
);
}
// const getObjectRatio = () => {
// const width = eventOverlay.data.box[2] * cameraConfig.detect.width;
// const height = eventOverlay.data.box[3] * cameraConfig.detect.height;
// return Math.round(100 * (width / height)) / 100;
// };
// return (
// <Fragment>
// <div
// className="absolute border-4 border-red-600"
// onMouseEnter={() => setIsHovering(true)}
// onMouseLeave={() => setIsHovering(false)}
// onTouchStart={() => setIsHovering(true)}
// onTouchEnd={() => setIsHovering(false)}
// style={{
// left: `${boxLeftEdge}%`,
// top: `${boxTopEdge}%`,
// right: `${boxRightEdge}%`,
// bottom: `${boxBottomEdge}%`,
// }}
// >
// {eventOverlay.class_type == 'entered_zone' ? (
// <div className="absolute w-2 h-2 bg-yellow-500 left-[50%] -translate-x-1/2 translate-y-3/4 bottom-0" />
// ) : null}
// </div>
// {isHovering && (
// <div className="absolute bg-white dark:bg-slate-800 p-4 block text-black dark:text-white text-lg" style={getHoverStyle()}>
// <div>{`Area: ${getObjectArea()} px`}</div>
// <div>{`Ratio: ${getObjectRatio()}`}</div>
// </div>
// )}
// </Fragment>
// );
// }

View File

@ -1,218 +1,220 @@
import { h } from 'preact';
import useSWR from 'swr';
import ActivityIndicator from './ActivityIndicator';
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
import About from '../icons/About';
import ActiveObjectIcon from '../icons/ActiveObject';
import PlayIcon from '../icons/Play';
import ExitIcon from '../icons/Exit';
import StationaryObjectIcon from '../icons/StationaryObject';
import FaceIcon from '../icons/Face';
import LicensePlateIcon from '../icons/LicensePlate';
import DeliveryTruckIcon from '../icons/DeliveryTruck';
import ZoneIcon from '../icons/Zone';
import { useMemo, useState } from 'preact/hooks';
import Button from './Button';
// import { h } from 'preact';
// import useSWR from 'swr';
// import ActivityIndicator from './ActivityIndicator';
// import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
// import About from '../icons/About';
// import ActiveObjectIcon from '../icons/ActiveObject';
// import PlayIcon from '../icons/Play';
// import ExitIcon from '../icons/Exit';
// import StationaryObjectIcon from '../icons/StationaryObject';
// import FaceIcon from '../icons/Face';
// import LicensePlateIcon from '../icons/LicensePlate';
// import DeliveryTruckIcon from '../icons/DeliveryTruck';
// import ZoneIcon from '../icons/Zone';
// import { useMemo, useState } from 'preact/hooks';
// import Button from './Button';
export default function TimelineSummary({ event, onFrameSelected }) {
const { data: eventTimeline } = useSWR([
'timeline',
{
source_id: event.id,
},
]);
export {}
const { data: config } = useSWR('config');
// export default function TimelineSummary({ event, onFrameSelected }) {
// const { data: eventTimeline } = useSWR([
// 'timeline',
// {
// source_id: event.id,
// },
// ]);
const annotationOffset = useMemo(() => {
if (!config) {
return 0;
}
// const { data: config } = useSWR('config');
return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
}, [config, event]);
// const annotationOffset = useMemo(() => {
// if (!config) {
// return 0;
// }
const [timeIndex, setTimeIndex] = useState(-1);
// return (config.cameras[event.camera]?.detect?.annotation_offset || 0) / 1000;
// }, [config, event]);
const recordingParams = useMemo(() => {
if (!event.end_time) {
return {
after: event.start_time,
};
}
// const [timeIndex, setTimeIndex] = useState(-1);
return {
before: event.end_time,
after: event.start_time,
};
}, [event]);
const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
// const recordingParams = useMemo(() => {
// if (!event.end_time) {
// return {
// after: event.start_time,
// };
// }
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
const getSeekSeconds = (seekUnix) => {
if (!recordings) {
return 0;
}
// return {
// before: event.end_time,
// after: event.start_time,
// };
// }, [event]);
// const { data: recordings } = useSWR([`${event.camera}/recordings`, recordingParams], { revalidateOnFocus: false });
let seekSeconds = 0;
recordings.every((segment) => {
// if the next segment is past the desired time, stop calculating
if (segment.start_time > seekUnix) {
return false;
}
// // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
// const getSeekSeconds = (seekUnix) => {
// if (!recordings) {
// return 0;
// }
if (segment.end_time < seekUnix) {
seekSeconds += segment.end_time - segment.start_time;
return true;
}
// let seekSeconds = 0;
// recordings.every((segment) => {
// // if the next segment is past the desired time, stop calculating
// if (segment.start_time > seekUnix) {
// return false;
// }
seekSeconds += segment.end_time - segment.start_time - (segment.end_time - seekUnix);
return true;
});
// if (segment.end_time < seekUnix) {
// seekSeconds += segment.end_time - segment.start_time;
// return true;
// }
return seekSeconds;
};
// seekSeconds += segment.end_time - segment.start_time - (segment.end_time - seekUnix);
// return true;
// });
const onSelectMoment = async (index) => {
setTimeIndex(index);
onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
};
// return seekSeconds;
// };
if (!eventTimeline || !config) {
return <ActivityIndicator />;
}
// const onSelectMoment = async (index) => {
// setTimeIndex(index);
// onFrameSelected(eventTimeline[index], getSeekSeconds(eventTimeline[index].timestamp + annotationOffset));
// };
if (eventTimeline.length == 0) {
return <div />;
}
// if (!eventTimeline || !config) {
// return <ActivityIndicator />;
// }
return (
<div className="flex flex-col">
<div className="h-14 flex justify-center">
<div className="flex flex-row flex-nowrap justify-between overflow-auto">
{eventTimeline.map((item, index) => (
<Button
key={index}
className="rounded-full"
type="iconOnly"
color={index == timeIndex ? 'blue' : 'gray'}
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
onClick={() => onSelectMoment(index)}
>
{getTimelineIcon(item)}
</Button>
))}
</div>
</div>
{timeIndex >= 0 ? (
<div className="m-2 max-w-md self-center">
<div className="flex justify-start">
<div className="text-lg flex justify-between py-4">Bounding boxes may not align</div>
<Button
className="rounded-full"
type="text"
color="gray"
aria-label=" Disclaimer: This data comes from the detect feed but is shown on the recordings, it is unlikely that the
streams are perfectly in sync so the bounding box and the footage will not line up perfectly. The annotation_offset field can be used to adjust this."
>
<About className="w-4" />
</Button>
</div>
</div>
) : null}
</div>
);
}
// if (eventTimeline.length == 0) {
// return <div />;
// }
function getTimelineIcon(timelineItem) {
switch (timelineItem.class_type) {
case 'visible':
return <PlayIcon className="w-8" />;
case 'gone':
return <ExitIcon className="w-8" />;
case 'active':
return <ActiveObjectIcon className="w-8" />;
case 'stationary':
return <StationaryObjectIcon className="w-8" />;
case 'entered_zone':
return <ZoneIcon className="w-8" />;
case 'attribute':
switch (timelineItem.data.attribute) {
case 'face':
return <FaceIcon className="w-8" />;
case 'license_plate':
return <LicensePlateIcon className="w-8" />;
default:
return <DeliveryTruckIcon className="w-8" />;
}
case 'sub_label':
switch (timelineItem.data.label) {
case 'person':
return <FaceIcon className="w-8" />;
case 'car':
return <LicensePlateIcon className="w-8" />;
}
}
}
// return (
// <div className="flex flex-col">
// <div className="h-14 flex justify-center">
// <div className="flex flex-row flex-nowrap justify-between overflow-auto">
// {eventTimeline.map((item, index) => (
// <Button
// key={index}
// className="rounded-full"
// type="iconOnly"
// color={index == timeIndex ? 'blue' : 'gray'}
// aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
// onClick={() => onSelectMoment(index)}
// >
// {getTimelineIcon(item)}
// </Button>
// ))}
// </div>
// </div>
// {timeIndex >= 0 ? (
// <div className="m-2 max-w-md self-center">
// <div className="flex justify-start">
// <div className="text-lg flex justify-between py-4">Bounding boxes may not align</div>
// <Button
// className="rounded-full"
// type="text"
// color="gray"
// aria-label=" Disclaimer: This data comes from the detect feed but is shown on the recordings, it is unlikely that the
// streams are perfectly in sync so the bounding box and the footage will not line up perfectly. The annotation_offset field can be used to adjust this."
// >
// <About className="w-4" />
// </Button>
// </div>
// </div>
// ) : null}
// </div>
// );
// }
function getTimelineItemDescription(config, timelineItem, event) {
switch (timelineItem.class_type) {
case 'visible':
return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
case 'entered_zone':
return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
.join(' and ')
.replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
case 'active':
return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
case 'stationary':
return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
case 'attribute': {
let title = "";
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
title = `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label}`;
} else {
title = `${event.label} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`
}
return `${title} at ${formatUnixTimestampToDateTime(
timelineItem.timestamp,
{
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
}
)}`;
}
case 'sub_label':
return `${event.label} recognized as ${timelineItem.data.sub_label} at ${formatUnixTimestampToDateTime(
timelineItem.timestamp,
{
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
}
)}`;
case 'gone':
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
}
}
// function getTimelineIcon(timelineItem) {
// switch (timelineItem.class_type) {
// case 'visible':
// return <PlayIcon className="w-8" />;
// case 'gone':
// return <ExitIcon className="w-8" />;
// case 'active':
// return <ActiveObjectIcon className="w-8" />;
// case 'stationary':
// return <StationaryObjectIcon className="w-8" />;
// case 'entered_zone':
// return <ZoneIcon className="w-8" />;
// case 'attribute':
// switch (timelineItem.data.attribute) {
// case 'face':
// return <FaceIcon className="w-8" />;
// case 'license_plate':
// return <LicensePlateIcon className="w-8" />;
// default:
// return <DeliveryTruckIcon className="w-8" />;
// }
// case 'sub_label':
// switch (timelineItem.data.label) {
// case 'person':
// return <FaceIcon className="w-8" />;
// case 'car':
// return <LicensePlateIcon className="w-8" />;
// }
// }
// }
// function getTimelineItemDescription(config, timelineItem, event) {
// switch (timelineItem.class_type) {
// case 'visible':
// return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// })}`;
// case 'entered_zone':
// return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
// .join(' and ')
// .replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// })}`;
// case 'active':
// return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// })}`;
// case 'stationary':
// return `${event.label} became stationary at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// })}`;
// case 'attribute': {
// let title = "";
// if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
// title = `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label}`;
// } else {
// title = `${event.label} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`
// }
// return `${title} at ${formatUnixTimestampToDateTime(
// timelineItem.timestamp,
// {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// }
// )}`;
// }
// case 'sub_label':
// return `${event.label} recognized as ${timelineItem.data.sub_label} at ${formatUnixTimestampToDateTime(
// timelineItem.timestamp,
// {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// }
// )}`;
// case 'gone':
// return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
// date_style: 'short',
// time_style: 'medium',
// time_format: config.ui.time_format,
// })}`;
// }
// }

View File

@ -1,62 +1,64 @@
import { createPortal } from 'react'; // TODO implement
import { useLayoutEffect, useRef, useState } from 'react';
// import { createPortal } from 'react'; // TODO implement
// import { useLayoutEffect, useRef, useState } from 'react';
const TIP_SPACE = 20;
// const TIP_SPACE = 20;
export default function Tooltip({ relativeTo, text, capitalize }) {
const [position, setPosition] = useState({ top: -9999, left: -9999 });
const portalRoot = document.getElementById('tooltips');
const ref = useRef();
export {}
useLayoutEffect(() => {
if (ref && ref.current && relativeTo && relativeTo.current) {
const windowWidth = window.innerWidth;
const {
x: relativeToX,
y: relativeToY,
width: relativeToWidth,
height: relativeToHeight,
} = relativeTo.current.getBoundingClientRect();
const { width: _tipWidth, height: _tipHeight } = ref.current.getBoundingClientRect();
const tipWidth = _tipWidth * 1.1;
const tipHeight = _tipHeight * 1.1;
// export default function Tooltip({ relativeTo, text, capitalize }) {
// const [position, setPosition] = useState({ top: -9999, left: -9999 });
// const portalRoot = document.getElementById('tooltips');
// const ref = useRef();
const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
// useLayoutEffect(() => {
// if (ref && ref.current && relativeTo && relativeTo.current) {
// const windowWidth = window.innerWidth;
// const {
// x: relativeToX,
// y: relativeToY,
// width: relativeToWidth,
// height: relativeToHeight,
// } = relativeTo.current.getBoundingClientRect();
// const { width: _tipWidth, height: _tipHeight } = ref.current.getBoundingClientRect();
// const tipWidth = _tipWidth * 1.1;
// const tipHeight = _tipHeight * 1.1;
let newTop = top - TIP_SPACE - tipHeight;
let newLeft = left - Math.round(tipWidth / 2);
// too far right
if (newLeft + tipWidth + TIP_SPACE > windowWidth - window.scrollX) {
newLeft = Math.max(0, left - tipWidth - TIP_SPACE);
newTop = top - Math.round(tipHeight / 2);
}
// too far left
else if (newLeft < TIP_SPACE + window.scrollX) {
newLeft = left + TIP_SPACE;
newTop = top - Math.round(tipHeight / 2);
}
// too close to top
else if (newTop <= TIP_SPACE + window.scrollY) {
newTop = top + tipHeight + TIP_SPACE;
}
// const left = relativeToX + Math.round(relativeToWidth / 2) + window.scrollX;
// const top = relativeToY + Math.round(relativeToHeight / 2) + window.scrollY;
setPosition({ left: newLeft, top: newTop });
}
}, [relativeTo, ref]);
// let newTop = top - TIP_SPACE - tipHeight;
// let newLeft = left - Math.round(tipWidth / 2);
// // too far right
// if (newLeft + tipWidth + TIP_SPACE > windowWidth - window.scrollX) {
// newLeft = Math.max(0, left - tipWidth - TIP_SPACE);
// newTop = top - Math.round(tipHeight / 2);
// }
// // too far left
// else if (newLeft < TIP_SPACE + window.scrollX) {
// newLeft = left + TIP_SPACE;
// newTop = top - Math.round(tipHeight / 2);
// }
// // too close to top
// else if (newTop <= TIP_SPACE + window.scrollY) {
// newTop = top + tipHeight + TIP_SPACE;
// }
const tooltip = (
<div
role="tooltip"
className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-transform transition-opacity duration-75 transform scale-90 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
capitalize ? 'capitalize' : ''
} ${position.top >= 0 ? 'opacity-100 scale-100' : ''}`}
ref={ref}
style={position}
>
{text}
</div>
);
// setPosition({ left: newLeft, top: newTop });
// }
// }, [relativeTo, ref]);
return portalRoot ? createPortal(tooltip, portalRoot) : tooltip;
}
// const tooltip = (
// <div
// role="tooltip"
// className={`shadow max-w-lg absolute pointer-events-none bg-gray-900 dark:bg-gray-200 bg-opacity-80 rounded px-2 py-1 transition-transform transition-opacity duration-75 transform scale-90 opacity-0 text-gray-100 dark:text-gray-900 text-sm ${
// capitalize ? 'capitalize' : ''
// } ${position.top >= 0 ? 'opacity-100 scale-100' : ''}`}
// ref={ref}
// style={position}
// >
// {text}
// </div>
// );
// return portalRoot ? createPortal(tooltip, portalRoot) : tooltip;
// }

View File

@ -1,80 +1,81 @@
import * as React from "react"
// import * as React from "react"
export {}
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn(
// "rounded-lg border bg-card text-card-foreground shadow-sm",
// className
// )}
{...props}
/>
))
Card.displayName = "Card"
// const Card = React.forwardRef<
// HTMLDivElement,
// React.HTMLAttributes<HTMLDivElement>
// >(({ className, ...props }, ref) => (
// <div
// ref={ref}
// // className={cn(
// // "rounded-lg border bg-card text-card-foreground shadow-sm",
// // className
// // )}
// {...props}
// />
// ))
// Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
// const CardHeader = React.forwardRef<
// HTMLDivElement,
// React.HTMLAttributes<HTMLDivElement>
// >(({ className, ...props }, ref) => (
// <div
// ref={ref}
// // className={cn("flex flex-col space-y-1.5 p-6", className)}
// {...props}
// />
// ))
// CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
// className={cn(
// "text-2xl font-semibold leading-none tracking-tight",
// className
// )}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
// const CardTitle = React.forwardRef<
// HTMLParagraphElement,
// React.HTMLAttributes<HTMLHeadingElement>
// >(({ className, ...props }, ref) => (
// <h3
// ref={ref}
// // className={cn(
// // "text-2xl font-semibold leading-none tracking-tight",
// // className
// // )}
// {...props}
// />
// ))
// CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
// className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
// const CardDescription = React.forwardRef<
// HTMLParagraphElement,
// React.HTMLAttributes<HTMLParagraphElement>
// >(({ className, ...props }, ref) => (
// <p
// ref={ref}
// // className={cn("text-sm text-muted-foreground", className)}
// {...props}
// />
// ))
// CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref}
// className= {cn("p-6 pt-0", className)}
{...props} />
))
CardContent.displayName = "CardContent"
// const CardContent = React.forwardRef<
// HTMLDivElement,
// React.HTMLAttributes<HTMLDivElement>
// >(({ className, ...props }, ref) => (
// <div ref={ref}
// // className= {cn("p-6 pt-0", className)}
// {...props} />
// ))
// CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
// const CardFooter = React.forwardRef<
// HTMLDivElement,
// React.HTMLAttributes<HTMLDivElement>
// >(({ className, ...props }, ref) => (
// <div
// ref={ref}
// // className={cn("flex items-center p-6 pt-0", className)}
// {...props}
// />
// ))
// CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
// export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Center, DEFAULT_THEME } from '@mantine/core';
import CogwheelSVG from './svg/CogwheelSVG';
import CogwheelSVG from '../svg/CogwheelSVG';
const CogwheelLoader = () => {

View File

@ -15,7 +15,7 @@ export class RecordingsStore {
makeAutoObservable(this)
}
recordingSchema = z.object({
private _recordingSchema = z.object({
hostName: z.string(),
cameraName: z.string(),
hour: z.string(),
@ -31,7 +31,7 @@ export class RecordingsStore {
this._recordToPlay = value
}
getFullRecordForPlay(value: RecordForPlay) {
return this.recordingSchema.safeParse(value)
return this._recordingSchema.safeParse(value)
}
private _hostIdParam: string | undefined
@ -63,6 +63,11 @@ export class RecordingsStore {
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
this._selectedCamera = value
}
selectedStartDay: string = ''
selectedEndDay: string = ''
private _selectedRange: [Date | null, Date | null] = [null, null]
public get selectedRange(): [Date | null, Date | null] {
return this._selectedRange
}
public set selectedRange(value: [Date | null, Date | null]) {
this._selectedRange = value
}
}

View File

@ -1,10 +1,31 @@
export const strings = {
host: {
host: 'Хост',
hostArr: {
name: 'Имя хоста',
url: 'Адрес',
enabled: 'Включен',
},
// user section
player: {
startVideo: 'Вкл Видео',
stopVideo: 'Выкл Видео',
object: 'Объект',
duration: 'Длительность',
startTime: 'Начало',
endTime: 'Конец'
},
camera: 'Камера',
hour: 'Час',
minute: 'Минута',
second: 'Секунда',
events: 'События',
event: 'Событие',
notHaveEvents: 'Нет событий',
date: 'Дата',
day: 'День',
selectHost:'Выбери хост',
selectCamera: 'Выбери камеру',
selectDate: 'Выбери дату',
selectRange: 'Выбери период',
aboutMe: "Обо мне",
settings: "Настройки",
changeTheme: "Изменить тему",
@ -32,14 +53,14 @@ export const strings = {
selectDeliveryMethod: "Выберите метод доставки:",
pickUpByMyself: "Я заберу самостоятельно",
courierDelivery: "Доставка курьером",
deliveryPoint:"Адрес доставки",
deliveryPoint: "Адрес доставки",
selectYourDeliveryAddress: "Выберите свой адрес доставки:",
deliveryDate:"Дата доставки",
deliveryDate: "Дата доставки",
selectDeliveryDate: "Выберите дату доставки:",
enterQuantity: "Введите количество:",
quantity:"Количество",
tooltip_close:"нажмите Enter",
currency:"₽",
quantity: "Количество",
tooltip_close: "нажмите Enter",
currency: "₽",
category: "Категории:",
collapse: "Свернуть",
hide: "Скрыть",

View File

@ -30,6 +30,33 @@ export const unixTimeToDate = (unixTime: number) => {
return formattedDate;
}
/**
* @param date
* @returns string '2024-02-25'
*/
export const dateToQueryString = (date: Date): string => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const formattedMonth = month < 10 ? `0${month}` : month;
const day = date.getDate();
const formattedDay = day < 10 ? `0${day}` : day;
return `${year}-${formattedMonth}-${formattedDay}`;
}
export const parseQueryDateToDate = (dateQuery: string): Date | null => {
const match = dateQuery.match(/(\d{4})-(\d{2})-(\d{2})/);
if (match) {
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1;
const day = parseInt(match[3], 10);
return new Date(year, month, day);
}
return null;
}
/**
* @param day frigate format, e.g day: 2024-02-23
* @param hour frigate format, e.g hour: 22
@ -210,14 +237,14 @@ interface DurationToken {
* @param end_time: number|null - Unix timestamp for end time
* @returns string - duration or 'In Progress' if end time is not provided
*/
export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string => {
export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string | undefined => {
if (isNaN(start_time)) {
return 'Invalid start time';
return
}
let duration = 'In Progress';
if (end_time) {
if (isNaN(end_time)) {
return 'Invalid end time';
return
}
const start = fromUnixTime(start_time);
const end = fromUnixTime(end_time);

View File

@ -61,9 +61,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
}
const headTitle = [
{ propertyName: 'name', title: strings.host.name },
{ propertyName: 'host', title: strings.host.url },
{ propertyName: 'enabled', title: strings.host.enabled },
{ propertyName: 'name', title: strings.hostArr.name },
{ propertyName: 'host', title: strings.hostArr.url },
{ propertyName: 'enabled', title: strings.hostArr.enabled },
{ title: '', sorting: false },
]

View File

@ -1,14 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react';
import JSMpegPlayer from './JSMpegPlayer';
import MSEPlayer from './MsePlayer';
import { CameraConfig } from '../../../types/frigateConfig';
import { LivePlayerMode } from '../../../types/live';
import useCameraActivity from '../../../hooks/use-camera-activity';
import useCameraLiveMode from '../../../hooks/use-camera-live-mode';
import WebRtcPlayer from './WebRTCPlayer';
import JSMpegPlayer from '../shared/components/players/JSMpegPlayer';
import MSEPlayer from '../shared/components/players/MsePlayer';
import { CameraConfig } from '../types/frigateConfig';
import { LivePlayerMode } from '../types/live';
import useCameraActivity from '../hooks/use-camera-activity';
import useCameraLiveMode from '../hooks/use-camera-live-mode';
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer';
import { Flex } from '@mantine/core';
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../../../services/frigate.proxy/frigate.schema';
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
type LivePlayerProps = {
camera: GetCameraWHostWConfig;
@ -70,7 +70,7 @@ const Player = ({
player = (
<MSEPlayer
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
camera='Not yet implemented'
camera='Not yet implemented' // TODO implement player
playbackEnabled={cameraActive}
onPlaying={() => setLiveReady(true)}
wsUrl={wsUrl}

View File

@ -1,12 +1,9 @@
import React, { useContext, useEffect } from 'react';
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
import React, { useContext } from 'react';
import { observer } from 'mobx-react-lite';
import { Context } from '..';
import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { Center, Text } from '@mantine/core';
import CogwheelLoader from '../shared/components/CogwheelLoader';
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter';
import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter';
import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter';
interface RecordingsFiltersRightSideProps {
}
@ -15,58 +12,18 @@ const RecordingsFiltersRightSide = ({
}: RecordingsFiltersRightSideProps) => {
const { recordingsStore: recStore } = useContext(Context)
const { data: hosts, isError, isPending, isSuccess } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts
})
useEffect(() => {
if (!hosts) return
if (recStore.hostIdParam) {
recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam)
recStore.hostIdParam = undefined
}
}, [isSuccess])
if (isPending) return <CogwheelLoader />
if (isError) return <Center><Text>Loading error!</Text></Center>
if (!hosts || hosts.length < 1) return null
const hostItems: OneSelectItem[] = hosts
.filter(host => host.enabled)
.map(host => ({ value: host.id, label: host.name }))
const handleSelect = (id: string, value: string) => {
const host = hosts?.find(host => host.id === value)
if (!host) {
recStore.selectedHost = undefined
recStore.selectedCamera = undefined
return
}
if (recStore.selectedHost?.id !== host.id) {
recStore.selectedCamera = undefined
}
recStore.selectedHost = host
}
console.log('RecordingsFiltersRightSide rendered')
return (
<>
<OneSelectFilter
id='frigate-hosts'
label='Select host:'
spaceBetween='1rem'
value={recStore.selectedHost?.id || ''}
defaultValue={recStore.selectedHost?.id || ''}
data={hostItems}
onChange={handleSelect}
/>
<HostSelectFilter />
{recStore.selectedHost ?
<CameraSelectFilter
selectedHostId={recStore.selectedHost.id} />
:
<></>
: <></>
}
{recStore.selectedCamera ?
<DateRangeSelectFilter />
: <></>
}
</>

View 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);

View File

@ -6,24 +6,25 @@ import { useQuery } from '@tanstack/react-query';
import { Context } from '..';
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
import { host } from '../shared/env.const';
import CogwheelLoader from '../shared/components/CogwheelLoader';
import RetryError from '../pages/RetryError';
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
import RetryErrorPage from '../pages/RetryErrorPage';
import CenterLoader from '../shared/components/CenterLoader';
interface SelectedCameraListProps {
cameraId: string,
// cameraId: string,
}
const SelectedCameraList = ({
cameraId,
// cameraId,
}: SelectedCameraListProps) => {
const { recordingsStore: recStore } = useContext(Context)
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
queryKey: [frigateQueryKeys.getCameraWHost, cameraId],
queryKey: [frigateQueryKeys.getCameraWHost, recStore.selectedCamera?.id],
queryFn: async () => {
if (cameraId) {
return frigateApi.getCameraWHost(cameraId)
if (recStore.selectedCamera) {
return frigateApi.getCameraWHost(recStore.selectedCamera.id)
}
return null
}
@ -33,14 +34,18 @@ const SelectedCameraList = ({
cameraRefetch()
}
if (cameraPending) return <CogwheelLoader />
if (cameraError) return <RetryError onRetry={handleRetry} />
if (cameraPending) return <CenterLoader />
if (cameraError) return <RetryErrorPage onRetry={handleRetry} />
if (!camera?.frigateHost) return null
return (
<Flex w='100%' h='100%' direction='column' align='center'>
<Text>{camera.frigateHost.name} / {camera.name}</Text>
{
}
<Suspense>
<CameraAccordion camera={camera} host={camera.frigateHost} />
</Suspense>

View File

@ -5,7 +5,8 @@ import { useQuery } from '@tanstack/react-query';
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
import { Context } from '..';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from '../pages/RetryError';
import RetryErrorPage from '../pages/RetryErrorPage';
import { strings } from '../shared/strings/strings';
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
@ -39,14 +40,14 @@ const SelectedHostList = ({
}
if (hostPending) return <CenterLoader />
if (hostError) return <RetryError onRetry={handleRetry} />
if (hostError) return <RetryErrorPage onRetry={handleRetry} />
if (!host || host.cameras.length < 1) return null
const cameras = host.cameras.slice(0, 2).map(camera => {
return (
<Accordion.Item key={camera.id + 'Item'} value={camera.id}>
<Accordion.Control key={camera.id + 'Control'}>{camera.name}</Accordion.Control>
<Accordion.Control key={camera.id + 'Control'}>{strings.camera}: {camera.name}</Accordion.Control>
<Accordion.Panel key={camera.id + 'Panel'}>
{openCameraId === camera.id && (
<Suspense>
@ -60,7 +61,7 @@ const SelectedHostList = ({
return (
<Flex w='100%' h='100%' direction='column' align='center'>
<Text>{host.name}</Text>
<Text>{strings.host}: {host.name}</Text>
<Accordion
mt='1rem'
variant='separated'