fix rerenders at events and days items

This commit is contained in:
NlightN22 2024-03-03 01:47:37 +07:00
parent 4100f731d1
commit 839cb90092
17 changed files with 440 additions and 312 deletions

View File

@ -54,11 +54,18 @@ function App() {
return <RetryErrorPage backVisible={false} mainVisible={false} onRetry={() => auth.signinRedirect()} /> return <RetryErrorPage backVisible={false} mainVisible={false} onRetry={() => auth.signinRedirect()} />
} }
if (!auth.isAuthenticated && !auth.isLoading && authErrorCounter < maxErrorAuthConts) { if (!auth.isAuthenticated && !auth.isLoading && authErrorCounter < maxErrorAuthConts) {
if (hasAuthParams()) {
console.log('auth.isAuthenticated', auth.isAuthenticated)
console.log('auth.isLoading', auth.isLoading)
return <RetryErrorPage backVisible={false} mainVisible={false} onRetry={() => auth.signinRedirect()} />
} else {
console.log('Not authenticated! Redirect! auth ErrorCounter', authErrorCounter) console.log('Not authenticated! Redirect! auth ErrorCounter', authErrorCounter)
setAuthErrorCounter(prevCount => prevCount + 1); setAuthErrorCounter(prevCount => prevCount + 1);
auth.signinRedirect() auth.signinRedirect()
} }
}
if ((!hasAuthParams() && !auth.isAuthenticated && !auth.isLoading) || auth.error) { if ((!hasAuthParams() && !auth.isAuthenticated && !auth.isLoading) || auth.error) {
setAuthErrorCounter(prevCount => prevCount + 1) setAuthErrorCounter(prevCount => prevCount + 1)

View File

@ -1,16 +1,15 @@
import { useState, useContext, useEffect, useRef, useMemo } from 'react';
import { Flex, Text } from '@mantine/core'; import { Flex, Text } from '@mantine/core';
import { useLocation, useNavigate } from 'react-router-dom';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Context } from '..'; import { Context } from '..';
import { isProduction } from '../shared/env.const';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide'; import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide';
import SelectedCameraList from '../widgets/SelectedCameraList'; import SelectedCameraList from '../widgets/SelectedCameraList';
import SelectedHostList from '../widgets/SelectedHostList';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import SelectedDayList from '../widgets/SelectedDayList'; import SelectedDayList from '../widgets/SelectedDayList';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import SelectedHostList from '../widgets/SelectedHostList';
import { isProduction } from '../shared/env.const';
export const recordingsPageQuery = { export const recordingsPageQuery = {
@ -38,7 +37,6 @@ const RecordingsPage = () => {
const [hostId, setHostId] = useState<string>('') const [hostId, setHostId] = useState<string>('')
const [cameraId, setCameraId] = useState<string>('') const [cameraId, setCameraId] = useState<string>('')
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null]) const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
const [firstRender, setFirstRender] = useState(false)
useEffect(() => { useEffect(() => {
if (!executed.current) { if (!executed.current) {
@ -53,13 +51,13 @@ const RecordingsPage = () => {
const parsedEndDay = parseQueryDateToDate(paramEndDay) const parsedEndDay = parseQueryDateToDate(paramEndDay)
recStore.selectedRange = [parsedStartDay, parsedEndDay] recStore.selectedRange = [parsedStartDay, parsedEndDay]
} }
setFirstRender(true) executed.current = true
return () => { return () => {
sideBarsStore.setRightChildren(null) sideBarsStore.setRightChildren(null)
sideBarsStore.rightVisible = false sideBarsStore.rightVisible = false
} }
} }
}, [paramCameraId, paramEndDay, paramHostId, paramStartDay, recStore, sideBarsStore]) }, [])
useEffect(() => { useEffect(() => {
setHostId(recStore.filteredHost?.id || '') setHostId(recStore.filteredHost?.id || '')
@ -98,8 +96,6 @@ const RecordingsPage = () => {
if (!isProduction) console.log('RecordingsPage rendered') if (!isProduction) console.log('RecordingsPage rendered')
if (!firstRender) return <CenterLoader />
const [startDay, endDay] = period const [startDay, endDay] = period
if (startDay && endDay) { if (startDay && endDay) {
if (startDay.getDate() === endDay.getDate()) { // if select only one day if (startDay.getDate() === endDay.getDate()) { // if select only one day

View File

@ -40,11 +40,11 @@ const SettingsPage = () => {
const { isAdmin, isLoading: adminLoading } = useAdminRole() const { isAdmin, isLoading: adminLoading } = useAdminRole()
const ecryptedValue = '**********' const ecryptedTemplate = '**********'
const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => { const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => {
return data?.map(item => { return data?.map(item => {
const { value, encrypted, ...rest } = item const { value, encrypted, ...rest } = item
if (encrypted) return { value: ecryptedValue, encrypted, ...rest } if (encrypted && value) return { value: ecryptedTemplate, encrypted, ...rest }
return item return item
}) })
} }
@ -84,8 +84,8 @@ const SettingsPage = () => {
const configsToUpdate = Object.keys(formDataObj).map(key => { const configsToUpdate = Object.keys(formDataObj).map(key => {
const value = formDataObj[key] const value = formDataObj[key]
const currData = data?.find(val => val.key === key) const currData = data?.find(val => val.key === key)
const isEncrypted = value === ecryptedValue const notChangedEncrypted = value === ecryptedTemplate
if (currData && currData.encrypted && isEncrypted) { if (currData && currData.encrypted && notChangedEncrypted) {
return { return {
key, key,
value: currData.value value: currData.value
@ -121,7 +121,7 @@ const SettingsPage = () => {
label={config.description} label={config.description}
value={config.value} value={config.value}
placeholder={config.description} placeholder={config.description}
ecryptedValue={ecryptedValue} ecryptedValue={ecryptedTemplate}
/> />
))} ))}
<Space h='2%' /> <Space h='2%' />

View File

@ -3,12 +3,13 @@ import { proxyURL } from "../../shared/env.const"
import { import {
GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost,
GetCameraWHostWConfig, GetRole, GetCameraWHostWConfig, GetRole,
GetRoleWCameras, GetExportedFile GetRoleWCameras, GetExportedFile, recordingSchema
} from "./frigate.schema"; } from "./frigate.schema";
import { FrigateConfig } from "../../types/frigateConfig"; import { FrigateConfig } from "../../types/frigateConfig";
import { RecordSummary } from "../../types/record"; import { RecordSummary } from "../../types/record";
import { EventFrigate } from "../../types/event"; import { EventFrigate } from "../../types/event";
import { keycloakConfig } from "../.."; import { keycloakConfig } from "../..";
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
export const getToken = (): string | undefined => { export const getToken = (): string | undefined => {
@ -67,7 +68,8 @@ export const proxyApi = {
getHostConfig: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/config`).then(res => res.data), getHostConfig: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/config`).then(res => res.data),
getImageFrigate: async (imageUrl: string) => { getImageFrigate: async (imageUrl: string) => {
const response = await instanceApi.get<Blob>(imageUrl, { const response = await instanceApi.get<Blob>(imageUrl, {
responseType: 'blob' responseType: 'blob',
timeout: 10 * 1000
}) })
return response.data return response.data
}, },
@ -144,10 +146,21 @@ export const proxyApi = {
eventThumbnailUrl: (hostName: string, eventId: string) => `${proxyPrefix}${hostName}/api/events/${eventId}/thumbnail.jpg`, eventThumbnailUrl: (hostName: string, eventId: string) => `${proxyPrefix}${hostName}/api/events/${eventId}/thumbnail.jpg`,
eventDownloadURL: (hostName: string, eventId: string) => `${proxyPrefix}${hostName}/api/events/${eventId}/clip.mp4?download=true`, eventDownloadURL: (hostName: string, eventId: string) => `${proxyPrefix}${hostName}/api/events/${eventId}/clip.mp4?download=true`,
// http://127.0.0.1:5000/vod/2024-02/23/19/CameraName/Asia,Krasnoyarsk/master.m3u8 // http://127.0.0.1:5000/vod/2024-02/23/19/CameraName/Asia,Krasnoyarsk/master.m3u8
recordingURL: (hostName: string, cameraName: string, timezone: string, day: string, hour: string) => {// day:2024-02-23 hour:19 recordingURL: (hostName?: string, cameraName?: string, timezone?: string, day?: string, hour?: string) => {// day:2024-02-23 hour:19
const parts = day.split('-') const record = {
hostName: hostName,
cameraName: cameraName,
day: day,
hour: hour,
timezone: getResolvedTimeZone().replace('/', ','),
}
const parsed = recordingSchema.safeParse(record)
if (parsed.success) {
const parts = parsed.data.day.split('-')
const date = `${parts[0]}-${parts[1]}/${parts[2]}/${hour}` const date = `${parts[0]}-${parts[1]}/${parts[2]}/${hour}`
return `${proxyPrefix}${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8` return `${proxyPrefix}${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8`
}
return undefined
}, },
postExportVideoTask: (hostName: string, cameraName: string, startUnixTime: number, endUnixTime: number) => { postExportVideoTask: (hostName: string, cameraName: string, startUnixTime: number, endUnixTime: number) => {
const url = `proxy/${hostName}/api/export/${cameraName}/start/${startUnixTime}/end/${endUnixTime}` const url = `proxy/${hostName}/api/export/${cameraName}/start/${startUnixTime}/end/${endUnixTime}`

View File

@ -105,6 +105,14 @@ export const getExpotedFile = z.object({
size: z.number(), size: z.number(),
}) })
export const recordingSchema = z.object({
hostName: z.string(),
cameraName: z.string(),
hour: z.string(),
day: z.string(),
timezone: z.string(),
})
export type GetConfig = z.infer<typeof getConfigSchema> export type GetConfig = z.infer<typeof getConfigSchema>
export type PutConfig = z.infer<typeof putConfigSchema> export type PutConfig = z.infer<typeof putConfigSchema>
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema> export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>

View File

@ -1,5 +1,5 @@
import { Accordion, Center, Loader } from '@mantine/core'; import { Accordion, Center, Loader } from '@mantine/core';
import React, { useContext } from 'react'; import React, { useContext, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import DayAccordion from './DayAccordion'; import DayAccordion from './DayAccordion';
@ -28,11 +28,6 @@ const CameraAccordion = () => {
} }
}) })
if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} />
if (!data || !camera) return null
const recodItem = (record: RecordSummary) => ( const recodItem = (record: RecordSummary) => (
<Accordion.Item key={record.day} value={record.day}> <Accordion.Item key={record.day} value={record.day}>
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control> <Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control>
@ -42,7 +37,8 @@ const CameraAccordion = () => {
</Accordion.Item> </Accordion.Item>
) )
const days = () => { const days = useMemo(() => {
if (data && camera) {
const [startDate, endDate] = recStore.selectedRange const [startDate, endDate] = recStore.selectedRange
if (startDate && endDate) { if (startDate && endDate) {
return data return data
@ -58,14 +54,20 @@ const CameraAccordion = () => {
if ((startDate && endDate) || (!startDate && !endDate)) { if ((startDate && endDate) || (!startDate && !endDate)) {
return data.map(rec => recodItem(rec)) return data.map(rec => recodItem(rec))
} }
return []
} }
return []
}, [data, recStore.selectedRange])
if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} />
if (!data || !camera) return null
if (!isProduction) console.log('CameraAccordion rendered') if (!isProduction) console.log('CameraAccordion rendered')
return ( return (
<Accordion variant='separated' radius="md" w='100%'> <Accordion variant='separated' radius="md" w='100%'>
{days()} {days}
</Accordion> </Accordion>
) )
} }

View File

@ -1,112 +1,65 @@
import { Accordion, Center, Flex, Group, NavLink, Progress, Text, UnstyledButton } from '@mantine/core'; import { Accordion, Text } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import { RecordSummary } from '../../../types/record';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import PlayControl from '../buttons/PlayControl'; import React, { useCallback, useContext, useMemo, useState } from 'react';
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { Context } from '../../..'; import { Context } from '../../..';
import VideoPlayer from '../players/VideoPlayer'; import { mapHostToHostname } from '../../../services/frigate.proxy/frigate.api';
import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil'; import { RecordSummary } from '../../../types/record';
import DayEventsAccordion from './DayEventsAccordion';
import { strings } from '../../strings/strings';
import { useNavigate } from 'react-router-dom';
import AccordionControlButton from '../buttons/AccordionControlButton';
import { IconExternalLink, IconShare } from '@tabler/icons-react';
import { routesPath } from '../../../router/routes.path';
import AccordionShareButton from '../buttons/AccordionShareButton';
import VideoDownloader from '../../../widgets/VideoDownloader';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import DayAccordionItem from './DayAccordionItem';
interface RecordingAccordionProps { interface RecordingAccordionProps {
recordSummary?: RecordSummary recordSummary?: RecordSummary,
} }
const DayAccordionItemMemo = React.memo(DayAccordionItem)
const DayAccordion = ({ const DayAccordion = ({
recordSummary recordSummary,
}: RecordingAccordionProps) => { }: RecordingAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const [playedValue, setVideoPlayerState] = useState<string>()
const [openedValue, setOpenedValue] = useState<string>() const [openedValue, setOpenedValue] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
const navigate = useNavigate()
const camera = recStore.openedCamera || recStore.filteredCamera const camera = recStore.openedCamera || recStore.filteredCamera
const hostName = mapHostToHostname(recStore.filteredHost) const hostName = mapHostToHostname(recStore.filteredHost)
const createRecordURL = (recordId: string): string | undefined => { const handleOpenPlayer = useCallback((value?: string) => {
const record = { if (recStore.playedItem !== value) {
hostName: hostName ? hostName : '', setOpenedValue(value);
cameraName: camera?.name, recStore.playedItem = value;
day: recordSummary?.day, } else if (openedValue === value && recStore.playedItem === value) {
hour: recordId, recStore.playedItem = undefined;
timezone: getResolvedTimeZone().replace('/', ','),
}
const parsed = recStore.getFullRecordForPlay(record)
if (parsed.success) {
return proxyApi.recordingURL(
parsed.data.hostName,
parsed.data.cameraName,
parsed.data.timezone,
parsed.data.day,
parsed.data.hour
)
}
return undefined
} }
}, [openedValue, recStore]);
useEffect(() => { const dayItems = useMemo(() => {
if (playedValue) { if (recordSummary && recordSummary.hours.length > 0) {
const url = createRecordURL(playedValue) return recordSummary.hours.map(hour => {
if (url) { const played = recordSummary.day + hour.hour === recStore.playedItem;
if (!isProduction) console.log('GET URL: ', url) return (
setPlayerUrl(url) <DayAccordionItemMemo
key={recordSummary.day + hour.hour}
recordSummary={recordSummary}
recordHour={hour}
hostName={hostName}
cameraName={camera?.name}
played={played}
openPlayer={handleOpenPlayer}
/>
);
});
} }
} else { return [];
setPlayerUrl(undefined) }, [recordSummary, hostName, camera, recStore.playedItem, handleOpenPlayer])
}
}, [playedValue])
if (!recordSummary || recordSummary.hours.length < 1) return <Text>Not have record at that day</Text> if (!recordSummary || recordSummary.hours.length < 1) return <Text>Not have record at {recordSummary?.day}</Text>
const handleOpenPlayer = (value: string) => {
if (playedValue !== value) {
setOpenedValue(value)
setVideoPlayerState(value)
} else if (openedValue === value && playedValue === value) {
setVideoPlayerState(undefined)
}
}
const handleOpenItem = (value: string) => { const handleOpenItem = (value: string) => {
if (openedValue === value) { setOpenedValue(value !== openedValue ? value : undefined)
setOpenedValue(undefined) recStore.playedItem = undefined
} else {
setOpenedValue(value)
}
setVideoPlayerState(undefined)
} }
if (!isProduction) console.log('DayAccordion rendered') if (!isProduction) 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>
)
const hanleOpenNewLink = (recordId: string) => {
const link = createRecordURL(recordId)
if (link) {
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
navigate(url)
}
}
return ( return (
<Accordion <Accordion
key={recordSummary.day} key={recordSummary.day}
@ -115,45 +68,7 @@ const DayAccordion = ({
value={openedValue} value={openedValue}
onChange={handleOpenItem} onChange={handleOpenItem}
> >
{recordSummary.hours.map(hour => ( {dayItems}
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
<Accordion.Control key={hour.hour + 'Control'}>
<Flex justify='space-between'>
{hourLabel(hour.hour, hour.events)}
<Group>
<AccordionShareButton recordUrl={createRecordURL(hour.hour)} />
<AccordionControlButton onClick={() => hanleOpenNewLink(hour.hour)}>
<IconExternalLink />
</AccordionControlButton>
<PlayControl
value={hour.hour}
playedValue={playedValue}
onClick={handleOpenPlayer} />
</Group>
</Flex>
</Accordion.Control>
<Accordion.Panel key={hour.hour + 'Panel'}>
{playedValue === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
{ }
{recStore.filteredHost && camera && hostName ?
<Flex w='100%' justify='center' mb='0.5rem'>
<VideoDownloader
cameraName={camera.name}
hostName={hostName}
startUnixTime={mapDateHourToUnixTime(recordSummary.day, hour.hour)[0]}
endUnixTime={mapDateHourToUnixTime(recordSummary.day, hour.hour)[1]}
/>
</Flex>
: ''}
{hour.events > 0 ?
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
:
<Center><Text>{strings.notHaveEvents}</Text></Center>
}
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion> </Accordion>
) )

View File

@ -0,0 +1,108 @@
import { Accordion, Flex, Group, Text } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { RecordHour, RecordSummary } from '../../../types/record';
import { isProduction } from '../../env.const';
import { strings } from '../../strings/strings';
import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil';
import AccordionControlButton from '../buttons/AccordionControlButton';
import AccordionShareButton from '../buttons/AccordionShareButton';
import PlayControl from '../buttons/PlayControl';
import DayPanel from './DayPanel';
interface DayAccordionItemProps {
recordSummary: RecordSummary,
recordHour: RecordHour,
hostName?: string,
cameraName?: string,
played?: boolean,
openPlayer?: (value?: string) => void,
}
const DayAccordionItem = ({
recordSummary,
recordHour,
hostName,
cameraName,
played,
openPlayer
}: DayAccordionItemProps) => {
const navigate = useNavigate()
const [playedURL, setPlayedUrl] = useState<string>()
const hour = recordHour.hour
const day = recordSummary.day
const item = day + hour
const recordURL = proxyApi.recordingURL(
hostName,
cameraName,
getResolvedTimeZone().replace('/', ','),
recordSummary.day,
hour,
)
const startUnixTime = mapDateHourToUnixTime(day, hour)[0]
const endUnixTime = mapDateHourToUnixTime(day, hour)[1]
const hanleOpenNewLink = () => {
if (recordURL) {
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordURL)}`
navigate(url)
}
}
const handleOpenPlayer = () => {
if (openPlayer) openPlayer(item)
}
useEffect(() => {
if (played) {
setPlayedUrl(recordURL)
} else {
setPlayedUrl(undefined)
}
}, [played])
if (!isProduction) console.log('DayAccordionItem rendered')
return (
<Accordion.Item value={item}>
<Accordion.Control key={hour + 'Control'}>
<Flex justify='space-between'>
<Group>
<Text>{strings.hour}: {hour}:00</Text>
{recordHour.events > 0 ?
<Text>{strings.events}: {recordHour.events}</Text>
:
<Text>{strings.notHaveEvents}</Text>
}
</Group>
<Group>
<AccordionShareButton recordUrl={recordURL} />
<AccordionControlButton onClick={hanleOpenNewLink}>
<IconExternalLink />
</AccordionControlButton>
<PlayControl
played={played ? played : false}
onClick={handleOpenPlayer} />
</Group>
</Flex>
</Accordion.Control>
<DayPanel
day={recordSummary.day}
hour={hour}
events={recordHour.events}
hostName={hostName}
cameraName={cameraName}
startUnixTime={startUnixTime}
endUnixTime={endUnixTime}
videoURL={recordURL}
playedURL={playedURL}
/>
</Accordion.Item>
);
};
export default DayAccordionItem;

View File

@ -0,0 +1,52 @@
import { Accordion, Center, Flex, Text } from '@mantine/core';
import VideoDownloader from '../../../widgets/VideoDownloader';
import { strings } from '../../strings/strings';
import VideoPlayer from '../players/VideoPlayer';
import DayEventsAccordion from './DayEventsAccordion';
interface DayPanelProps {
day: string,
hour: string,
events: number,
videoURL?: string,
hostName?: string,
cameraName?: string,
playedURL?: string,
startUnixTime: number,
endUnixTime: number,
}
const DayPanel = ({
day,
hour,
events,
hostName,
cameraName,
videoURL,
playedURL,
startUnixTime,
endUnixTime,
}: DayPanelProps) => {
return (
<Accordion.Panel key={hour + 'Panel'}>
{playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>}
{cameraName && hostName ?
<Flex w='100%' justify='center' mb='0.5rem'>
<VideoDownloader
cameraName={cameraName}
hostName={hostName}
startUnixTime={startUnixTime}
endUnixTime={endUnixTime}
/>
</Flex>
: ''}
{events > 0 ?
<DayEventsAccordion day={day} hour={hour} qty={events} />
:
<Center><Text>{strings.notHaveEvents}</Text></Center>
}
</Accordion.Panel>
);
};
export default DayPanel;

View File

@ -1,29 +1,29 @@
import { Flex, Button, Text } from '@mantine/core'; import { Button, Flex, Text } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react'; import { IconExternalLink } from '@tabler/icons-react';
import React from 'react';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil';
import VideoPlayer from '../players/VideoPlayer';
import { EventFrigate } from '../../../types/event'; import { EventFrigate } from '../../../types/event';
import { strings } from '../../strings/strings';
import { getDurationFromTimestamps, unixTimeToDate } from '../../utils/dateUtil';
import BlobImage from '../images/BlobImage'; import BlobImage from '../images/BlobImage';
import VideoPlayer from '../players/VideoPlayer';
interface EventPanelProps { interface EventPanelProps {
event: EventFrigate event: EventFrigate
playedValue?: string
playerUrl?: string
hostName?: string hostName?: string
videoURL?: string,
playedURL?: string,
} }
const EventPanel = ({ const EventPanel = ({
event, event,
playedValue,
playerUrl,
hostName, hostName,
videoURL,
playedURL,
}: EventPanelProps) => { }: EventPanelProps) => {
return ( return (
<> <>
{playedValue === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>} {playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>}
<Flex w='100%' justify='space-between'> <Flex w='100%' justify='space-between'>
{!hostName ? <></> : {!hostName ? <></> :
<BlobImage <BlobImage

View File

@ -1,22 +1,13 @@
import { Accordion, Center, Flex, Group, Loader, Text } from '@mantine/core'; import { Accordion, Center, Loader, Text } from '@mantine/core';
import { observer } from 'mobx-react-lite';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Context } from '../../..';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useState } from 'react';
import { Context } from '../../..';
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema'; import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
import PlayControl from '../buttons/PlayControl'; import { getUnixTime } from '../../utils/dateUtil';
import { getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../../utils/dateUtil';
import RetryError from '../RetryError'; import RetryError from '../RetryError';
import { strings } from '../../strings/strings'; import EventsAccordionItem from './EventsAccordionItem';
import { EventFrigate } from '../../../types/event';
import { IconExternalLink } from '@tabler/icons-react';
import { routesPath } from '../../../router/routes.path';
import AccordionControlButton from '../buttons/AccordionControlButton';
import AccordionShareButton from '../buttons/AccordionShareButton';
import { useNavigate } from 'react-router-dom';
import EventPanel from './EventPanel';
import { isProduction } from '../../env.const';
/** /**
* @param day frigate format, e.g day: 2024-02-23 * @param day frigate format, e.g day: 2024-02-23
@ -40,13 +31,9 @@ interface EventsAccordionProps {
const EventsAccordion = ({ const EventsAccordion = ({
day, day,
hour, hour,
// TODO labels, score
}: EventsAccordionProps) => { }: EventsAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const [playedValue, setPlayedValue] = useState<string>()
const [openedItem, setOpenedItem] = useState<string>() const [openedItem, setOpenedItem] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
const navigate = useNavigate()
const host = recStore.filteredHost const host = recStore.filteredHost
const hostName = mapHostToHostname(host) const hostName = mapHostToHostname(host)
@ -85,67 +72,29 @@ const EventsAccordion = ({
} }
}) })
const createEventUrl = useCallback((eventId: string) => {
if (hostName)
return proxyApi.eventURL(hostName, eventId)
return undefined
}, [hostName])
useEffect(() => {
if (playedValue) {
if (playedValue && host) {
const url = createEventUrl(playedValue)
if (!isProduction) console.log('GET EVENT URL: ', url)
setPlayerUrl(url)
}
} else {
setPlayerUrl(undefined)
}
}, [playedValue, createEventUrl, host])
if (isPending) return <Center><Loader /></Center> if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} /> if (isError) return <RetryError onRetry={refetch} />
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center> if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
const handleOpenPlayer = (openedValue: string) => { const handleOpenPlayer = (value: string | undefined) => {
if (openedValue !== playedValue) { if (value !== recStore.playedItem) {
setOpenedItem(openedValue) setOpenedItem(value)
setPlayedValue(openedValue) recStore.playedItem = value
} else if (openedValue === playedValue) { } else if (value === recStore.playedItem) {
setPlayedValue(undefined) recStore.playedItem = undefined
} }
} }
const handleOpenItem = (value: string) => { const handleOpenItem = (value: string) => {
if (playedValue === value) { if (openedItem === value) {
setOpenedItem(undefined) setOpenedItem(undefined)
} else { } else {
setOpenedItem(value) setOpenedItem(value)
} }
setPlayedValue(undefined) recStore.playedItem = undefined
} }
const eventLabel = (event: EventFrigate) => { if (!hostName) throw Error('EventsAccordion hostName must be exist')
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>
)
}
const hanleOpenNewLink = (recordId: string) => {
const link = createEventUrl(recordId)
if (link) {
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
navigate(url)
}
}
return ( return (
<Accordion <Accordion
@ -155,30 +104,12 @@ const EventsAccordion = ({
onChange={handleOpenItem} onChange={handleOpenItem}
> >
{data.map(event => ( {data.map(event => (
<Accordion.Item key={event.id + 'Item'} value={event.id}> <EventsAccordionItem
<Accordion.Control key={event.id + 'Control'}>
<Flex justify='space-between'>
{eventLabel(event)}
<Group>
<AccordionShareButton recordUrl={createEventUrl(event.id)} />
<AccordionControlButton onClick={() => hanleOpenNewLink(event.id)}>
<IconExternalLink />
</AccordionControlButton>
<PlayControl
value={event.id}
playedValue={playedValue}
onClick={handleOpenPlayer} />
</Group>
</Flex>
</Accordion.Control>
<Accordion.Panel key={event.id + 'Panel'}>
<EventPanel
event={event} event={event}
playedValue={playedValue} hostName={hostName}
playerUrl={playerUrl} played={recStore.playedItem === event.id}
hostName={hostName} /> openPlayer={handleOpenPlayer}
</Accordion.Panel> />
</Accordion.Item>
))} ))}
</Accordion> </Accordion>
); );

View File

@ -0,0 +1,103 @@
import { Accordion, Flex, Group, Text } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
import React, { useCallback, useEffect, useState } from 'react';
import AccordionControlButton from '../buttons/AccordionControlButton';
import AccordionShareButton from '../buttons/AccordionShareButton';
import PlayControl from '../buttons/PlayControl';
import EventPanel from './EventPanel';
import { EventFrigate } from '../../../types/event';
import { strings } from '../../strings/strings';
import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path';
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
interface EventsAccordionItemProps {
event: EventFrigate
hostName: string
played?: boolean
openPlayer?: (value?: string) => void,
}
const EventsAccordionItem = ({
event,
hostName,
played,
openPlayer,
} : EventsAccordionItemProps) => {
const [playedURL, setPlayedUrl] = useState<string>()
const navigate = useNavigate()
const createEventUrl = useCallback((eventId: string) => {
if (hostName)
return proxyApi.eventURL(hostName, eventId)
return undefined
}, [hostName])
const eventVideoURL = createEventUrl(event.id)
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>
)
}
const hanleOpenNewLink = () => {
const link = eventVideoURL
if (link) {
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
navigate(url)
}
}
const handleOpenPlayer = () => {
if (openPlayer) openPlayer(event.id)
}
useEffect(() => {
if (played) {
setPlayedUrl(eventVideoURL)
} else {
setPlayedUrl(undefined)
}
}, [played])
return (
<Accordion.Item key={event.id + 'Item'} value={event.id}>
<Accordion.Control key={event.id + 'Control'}>
<Flex justify='space-between'>
{eventLabel(event)}
<Group>
<AccordionShareButton recordUrl={eventVideoURL} />
<AccordionControlButton onClick={hanleOpenNewLink}>
<IconExternalLink />
</AccordionControlButton>
<PlayControl
played={played ? played : false }
onClick={handleOpenPlayer} />
</Group>
</Flex>
</Accordion.Control>
<Accordion.Panel key={event.id + 'Panel'}>
<EventPanel
event={event}
videoURL={eventVideoURL}
playedURL={playedURL}
hostName={hostName} />
</Accordion.Panel>
</Accordion.Item>
);
};
export default EventsAccordionItem;

View File

@ -1,6 +1,5 @@
import React from 'react'; import { Flex, createStyles } from '@mantine/core';
import { Flex, Group, Text, createStyles } from '@mantine/core'; import { IconPlayerPlayFilled, IconPlayerStopFilled } from '@tabler/icons-react';
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
import { strings } from '../../strings/strings'; import { strings } from '../../strings/strings';
import AccordionControlButton from './AccordionControlButton'; import AccordionControlButton from './AccordionControlButton';
@ -15,28 +14,26 @@ const useStyles = createStyles((theme) => ({
})) }))
interface PlayControlProps { interface PlayControlProps {
value: string, played: boolean,
playedValue?: string, onClick?: () => void
onClick?: (value: string) => void
} }
const PlayControl = ({ const PlayControl = ({
value, played,
playedValue,
onClick onClick
}: PlayControlProps) => { }: PlayControlProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const handleClick = (value: string) => { const handleClick = () => {
if (onClick) onClick(value) if (onClick) onClick()
} }
return ( return (
<AccordionControlButton <AccordionControlButton
onClick={() => { handleClick(value) }} onClick={() => { handleClick() }}
> >
<Flex align='center'> <Flex align='center'>
{playedValue === value ? strings.player.stopVideo : strings.player.startVideo} {played ? strings.player.stopVideo : strings.player.startVideo}
{playedValue === value ? {played ?
<IconPlayerStopFilled <IconPlayerStopFilled
className={classes.iconStop} /> className={classes.iconStop} />
: :

View File

@ -67,7 +67,7 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
}; };
} }
executed.current = true executed.current = true
}, [videoUrl]); }, []);
useEffect(() => { useEffect(() => {

View File

@ -10,28 +10,19 @@ export type RecordForPlay = {
timezone?: string timezone?: string
} }
export class RecordingsStore { export class RecordingsStore {
constructor() { constructor() {
makeAutoObservable(this) makeAutoObservable(this)
} }
private _recordingSchema = z.object({ private _playedURL: string | undefined
hostName: z.string(), public get playedItem(): string | undefined {
cameraName: z.string(), return this._playedURL
hour: z.string(), }
day: z.string(), public set playedItem(value: string | undefined) {
timezone: z.string(), this._playedURL = value
})
// private _recordToPlay: RecordForPlay = {}
// public get recordToPlay(): RecordForPlay {
// return this._recordToPlay
// }
// public set recordToPlay(value: RecordForPlay) {
// this._recordToPlay = value
// }
getFullRecordForPlay(value: RecordForPlay) {
return this._recordingSchema.safeParse(value)
} }
private _hostIdParam: string | undefined private _hostIdParam: string | undefined

View File

@ -3,15 +3,15 @@ import React, { useContext } from 'react';
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil'; import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
import { Context } from '..'; import { Context } from '..';
import { Flex, Text } from '@mantine/core'; import { Center, Flex, Text } from '@mantine/core';
import RetryErrorPage from '../pages/RetryErrorPage'; import RetryErrorPage from '../pages/RetryErrorPage';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import DayAccordion from '../shared/components/accordion/DayAccordion'; import DayAccordion from '../shared/components/accordion/DayAccordion';
import { isProduction } from '../shared/env.const';
interface SelectedDayListProps { interface SelectedDayListProps {
day: Date day: Date
} }
const SelectedDayList = ({ const SelectedDayList = ({
@ -20,16 +20,15 @@ const SelectedDayList = ({
const { recordingsStore: recStore } = useContext(Context) const { recordingsStore: recStore } = useContext(Context)
const camera = recStore.filteredCamera const camera = recStore.filteredCamera
const host = recStore.filteredHost const host = recStore.filteredHost
const playedItem = recStore.playedItem
const { data, isPending, isError, refetch } = useQuery({ const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day], queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
queryFn: async () => { queryFn: async () => {
if (camera && host) { if (camera && host) {
const stringDay = dateToQueryString(day)
const hostName = mapHostToHostname(host) const hostName = mapHostToHostname(host)
if (hostName){ if (hostName){
const res = await proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone()) return proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
return res.find(record => record.day === stringDay)
} }
} }
return null return null
@ -42,12 +41,18 @@ const SelectedDayList = ({
if (isPending) return <CenterLoader /> if (isPending) return <CenterLoader />
if (isError) return <RetryErrorPage onRetry={handleRetry} /> if (isError) return <RetryErrorPage onRetry={handleRetry} />
if (!camera || !host || !data) return <CenterLoader /> if (!camera || !host) return <CenterLoader />
if (!data) return <Text>Not have response from server</Text>
const stringDay = dateToQueryString(day)
const recordingsDay = data.find(record => record.day === stringDay)
if (!recordingsDay) return <Center><Text>Not have record at {stringDay}</Text></Center>
if (!isProduction) console.log('SelectedDayList rendered')
return ( return (
<Flex w='100%' h='100%' direction='column' align='center'> <Flex w='100%' h='100%' direction='column' align='center'>
<Text>{host.name} / {camera.name} / {data.day}</Text> <Text>{host.name} / {camera.name} / {stringDay}</Text>
<DayAccordion recordSummary={data} /> <DayAccordion recordSummary={recordingsDay} />
</Flex> </Flex>
); );
}; };

View File

@ -84,7 +84,7 @@ const VideoDownloader = ({
setTimer(undefined) setTimer(undefined)
}, 5 * 60 * 1000) }, 5 * 60 * 1000)
} }
}, [createName, link, videoBlob, checkVideo, getVideBlob, hostName, timer]) }, [createName, link, videoBlob])
useEffect(() => { useEffect(() => {
if (videoBlob && videoBlob instanceof Blob && createName) { if (videoBlob && videoBlob instanceof Blob && createName) {
@ -97,7 +97,7 @@ const VideoDownloader = ({
} }
} }
} }
}, [videoBlob, createName, link, deleteVideo]) }, [videoBlob, createName, link])
const checkTime = () => { const checkTime = () => {
const duration = endUnixTime - startUnixTime const duration = endUnixTime - startUnixTime