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()} />
}
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)
setAuthErrorCounter(prevCount => prevCount + 1);
auth.signinRedirect()
}
}
if ((!hasAuthParams() && !auth.isAuthenticated && !auth.isLoading) || auth.error) {
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 { useLocation, useNavigate } from 'react-router-dom';
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 { isProduction } from '../shared/env.const';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide';
import SelectedCameraList from '../widgets/SelectedCameraList';
import SelectedHostList from '../widgets/SelectedHostList';
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
import SelectedDayList from '../widgets/SelectedDayList';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import { isProduction } from '../shared/env.const';
import SelectedHostList from '../widgets/SelectedHostList';
export const recordingsPageQuery = {
@ -38,7 +37,6 @@ const RecordingsPage = () => {
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(() => {
if (!executed.current) {
@ -53,13 +51,13 @@ const RecordingsPage = () => {
const parsedEndDay = parseQueryDateToDate(paramEndDay)
recStore.selectedRange = [parsedStartDay, parsedEndDay]
}
setFirstRender(true)
executed.current = true
return () => {
sideBarsStore.setRightChildren(null)
sideBarsStore.rightVisible = false
}
}
}, [paramCameraId, paramEndDay, paramHostId, paramStartDay, recStore, sideBarsStore])
}, [])
useEffect(() => {
setHostId(recStore.filteredHost?.id || '')
@ -98,8 +96,6 @@ const RecordingsPage = () => {
if (!isProduction) 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

View File

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

View File

@ -3,12 +3,13 @@ import { proxyURL } from "../../shared/env.const"
import {
GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost,
GetCameraWHostWConfig, GetRole,
GetRoleWCameras, GetExportedFile
GetRoleWCameras, GetExportedFile, recordingSchema
} from "./frigate.schema";
import { FrigateConfig } from "../../types/frigateConfig";
import { RecordSummary } from "../../types/record";
import { EventFrigate } from "../../types/event";
import { keycloakConfig } from "../..";
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
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),
getImageFrigate: async (imageUrl: string) => {
const response = await instanceApi.get<Blob>(imageUrl, {
responseType: 'blob'
responseType: 'blob',
timeout: 10 * 1000
})
return response.data
},
@ -144,10 +146,21 @@ export const proxyApi = {
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`,
// 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
const parts = day.split('-')
recordingURL: (hostName?: string, cameraName?: string, timezone?: string, day?: string, hour?: string) => {// day:2024-02-23 hour:19
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}`
return `${proxyPrefix}${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8`
}
return undefined
},
postExportVideoTask: (hostName: string, cameraName: string, startUnixTime: number, endUnixTime: number) => {
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(),
})
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 PutConfig = z.infer<typeof putConfigSchema>
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>

View File

@ -1,5 +1,5 @@
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 { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
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) => (
<Accordion.Item key={record.day} value={record.day}>
<Accordion.Control key={record.day + 'control'}>{strings.day}: {record.day}</Accordion.Control>
@ -42,7 +37,8 @@ const CameraAccordion = () => {
</Accordion.Item>
)
const days = () => {
const days = useMemo(() => {
if (data && camera) {
const [startDate, endDate] = recStore.selectedRange
if (startDate && endDate) {
return data
@ -58,14 +54,20 @@ const CameraAccordion = () => {
if ((startDate && endDate) || (!startDate && !endDate)) {
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')
return (
<Accordion variant='separated' radius="md" w='100%'>
{days()}
{days}
</Accordion>
)
}

View File

@ -1,112 +1,65 @@
import { Accordion, Center, Flex, Group, NavLink, Progress, Text, UnstyledButton } from '@mantine/core';
import React, { useContext, useEffect, useState } from 'react';
import { RecordSummary } from '../../../types/record';
import { Accordion, Text } from '@mantine/core';
import { observer } from 'mobx-react-lite';
import PlayControl from '../buttons/PlayControl';
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import { Context } from '../../..';
import VideoPlayer from '../players/VideoPlayer';
import { getResolvedTimeZone, mapDateHourToUnixTime } from '../../utils/dateUtil';
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 { mapHostToHostname } from '../../../services/frigate.proxy/frigate.api';
import { RecordSummary } from '../../../types/record';
import { isProduction } from '../../env.const';
import DayAccordionItem from './DayAccordionItem';
interface RecordingAccordionProps {
recordSummary?: RecordSummary
recordSummary?: RecordSummary,
}
const DayAccordionItemMemo = React.memo(DayAccordionItem)
const DayAccordion = ({
recordSummary
recordSummary,
}: RecordingAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context)
const [playedValue, setVideoPlayerState] = useState<string>()
const [openedValue, setOpenedValue] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
const navigate = useNavigate()
const camera = recStore.openedCamera || recStore.filteredCamera
const hostName = mapHostToHostname(recStore.filteredHost)
const createRecordURL = (recordId: string): string | undefined => {
const record = {
hostName: hostName ? hostName : '',
cameraName: camera?.name,
day: recordSummary?.day,
hour: recordId,
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
const handleOpenPlayer = useCallback((value?: string) => {
if (recStore.playedItem !== value) {
setOpenedValue(value);
recStore.playedItem = value;
} else if (openedValue === value && recStore.playedItem === value) {
recStore.playedItem = undefined;
}
}, [openedValue, recStore]);
useEffect(() => {
if (playedValue) {
const url = createRecordURL(playedValue)
if (url) {
if (!isProduction) console.log('GET URL: ', url)
setPlayerUrl(url)
const dayItems = useMemo(() => {
if (recordSummary && recordSummary.hours.length > 0) {
return recordSummary.hours.map(hour => {
const played = recordSummary.day + hour.hour === recStore.playedItem;
return (
<DayAccordionItemMemo
key={recordSummary.day + hour.hour}
recordSummary={recordSummary}
recordHour={hour}
hostName={hostName}
cameraName={camera?.name}
played={played}
openPlayer={handleOpenPlayer}
/>
);
});
}
} else {
setPlayerUrl(undefined)
}
}, [playedValue])
return [];
}, [recordSummary, hostName, camera, recStore.playedItem, handleOpenPlayer])
if (!recordSummary || recordSummary.hours.length < 1) return <Text>Not have record at that day</Text>
const handleOpenPlayer = (value: string) => {
if (playedValue !== value) {
setOpenedValue(value)
setVideoPlayerState(value)
} else if (openedValue === value && playedValue === value) {
setVideoPlayerState(undefined)
}
}
if (!recordSummary || recordSummary.hours.length < 1) return <Text>Not have record at {recordSummary?.day}</Text>
const handleOpenItem = (value: string) => {
if (openedValue === value) {
setOpenedValue(undefined)
} else {
setOpenedValue(value)
}
setVideoPlayerState(undefined)
setOpenedValue(value !== openedValue ? value : undefined)
recStore.playedItem = undefined
}
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 (
<Accordion
key={recordSummary.day}
@ -115,45 +68,7 @@ const DayAccordion = ({
value={openedValue}
onChange={handleOpenItem}
>
{recordSummary.hours.map(hour => (
<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>
))}
{dayItems}
</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 React from 'react';
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 { strings } from '../../strings/strings';
import { getDurationFromTimestamps, unixTimeToDate } from '../../utils/dateUtil';
import BlobImage from '../images/BlobImage';
import VideoPlayer from '../players/VideoPlayer';
interface EventPanelProps {
event: EventFrigate
playedValue?: string
playerUrl?: string
hostName?: string
videoURL?: string,
playedURL?: string,
}
const EventPanel = ({
event,
playedValue,
playerUrl,
hostName,
videoURL,
playedURL,
}: EventPanelProps) => {
return (
<>
{playedValue === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
{playedURL && playedURL === videoURL ? <VideoPlayer videoUrl={playedURL} /> : <></>}
<Flex w='100%' justify='space-between'>
{!hostName ? <></> :
<BlobImage

View File

@ -1,22 +1,13 @@
import { Accordion, Center, Flex, Group, Loader, Text } from '@mantine/core';
import { observer } from 'mobx-react-lite';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Context } from '../../..';
import { Accordion, Center, Loader, Text } from '@mantine/core';
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 { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
import PlayControl from '../buttons/PlayControl';
import { getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../../utils/dateUtil';
import { getUnixTime } from '../../utils/dateUtil';
import RetryError from '../RetryError';
import { strings } from '../../strings/strings';
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';
import EventsAccordionItem from './EventsAccordionItem';
/**
* @param day frigate format, e.g day: 2024-02-23
@ -40,13 +31,9 @@ interface EventsAccordionProps {
const EventsAccordion = ({
day,
hour,
// TODO labels, score
}: EventsAccordionProps) => {
const { recordingsStore: recStore } = useContext(Context)
const [playedValue, setPlayedValue] = useState<string>()
const [openedItem, setOpenedItem] = useState<string>()
const [playerUrl, setPlayerUrl] = useState<string>()
const navigate = useNavigate()
const host = recStore.filteredHost
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 (isError) return <RetryError onRetry={refetch} />
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
const handleOpenPlayer = (openedValue: string) => {
if (openedValue !== playedValue) {
setOpenedItem(openedValue)
setPlayedValue(openedValue)
} else if (openedValue === playedValue) {
setPlayedValue(undefined)
const handleOpenPlayer = (value: string | undefined) => {
if (value !== recStore.playedItem) {
setOpenedItem(value)
recStore.playedItem = value
} else if (value === recStore.playedItem) {
recStore.playedItem = undefined
}
}
const handleOpenItem = (value: string) => {
if (playedValue === value) {
if (openedItem === value) {
setOpenedItem(undefined)
} else {
setOpenedItem(value)
}
setPlayedValue(undefined)
recStore.playedItem = 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>
)
}
const hanleOpenNewLink = (recordId: string) => {
const link = createEventUrl(recordId)
if (link) {
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
navigate(url)
}
}
if (!hostName) throw Error('EventsAccordion hostName must be exist')
return (
<Accordion
@ -155,30 +104,12 @@ const EventsAccordion = ({
onChange={handleOpenItem}
>
{data.map(event => (
<Accordion.Item key={event.id + 'Item'} value={event.id}>
<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
<EventsAccordionItem
event={event}
playedValue={playedValue}
playerUrl={playerUrl}
hostName={hostName} />
</Accordion.Panel>
</Accordion.Item>
hostName={hostName}
played={recStore.playedItem === event.id}
openPlayer={handleOpenPlayer}
/>
))}
</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, Group, Text, createStyles } from '@mantine/core';
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
import { Flex, createStyles } from '@mantine/core';
import { IconPlayerPlayFilled, IconPlayerStopFilled } from '@tabler/icons-react';
import { strings } from '../../strings/strings';
import AccordionControlButton from './AccordionControlButton';
@ -15,28 +14,26 @@ const useStyles = createStyles((theme) => ({
}))
interface PlayControlProps {
value: string,
playedValue?: string,
onClick?: (value: string) => void
played: boolean,
onClick?: () => void
}
const PlayControl = ({
value,
playedValue,
played,
onClick
}: PlayControlProps) => {
const { classes } = useStyles();
const handleClick = (value: string) => {
if (onClick) onClick(value)
const handleClick = () => {
if (onClick) onClick()
}
return (
<AccordionControlButton
onClick={() => { handleClick(value) }}
onClick={() => { handleClick() }}
>
<Flex align='center'>
{playedValue === value ? strings.player.stopVideo : strings.player.startVideo}
{playedValue === value ?
{played ? strings.player.stopVideo : strings.player.startVideo}
{played ?
<IconPlayerStopFilled
className={classes.iconStop} />
:

View File

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

View File

@ -10,28 +10,19 @@ export type RecordForPlay = {
timezone?: string
}
export class RecordingsStore {
constructor() {
makeAutoObservable(this)
}
private _recordingSchema = z.object({
hostName: z.string(),
cameraName: z.string(),
hour: z.string(),
day: z.string(),
timezone: z.string(),
})
// 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 _playedURL: string | undefined
public get playedItem(): string | undefined {
return this._playedURL
}
public set playedItem(value: string | undefined) {
this._playedURL = value
}
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 { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
import { Context } from '..';
import { Flex, Text } from '@mantine/core';
import { Center, Flex, Text } from '@mantine/core';
import RetryErrorPage from '../pages/RetryErrorPage';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import { observer } from 'mobx-react-lite';
import DayAccordion from '../shared/components/accordion/DayAccordion';
import { isProduction } from '../shared/env.const';
interface SelectedDayListProps {
day: Date
}
const SelectedDayList = ({
@ -20,16 +20,15 @@ const SelectedDayList = ({
const { recordingsStore: recStore } = useContext(Context)
const camera = recStore.filteredCamera
const host = recStore.filteredHost
const playedItem = recStore.playedItem
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
queryFn: async () => {
if (camera && host) {
const stringDay = dateToQueryString(day)
const hostName = mapHostToHostname(host)
if (hostName){
const res = await proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
return res.find(record => record.day === stringDay)
return proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
}
}
return null
@ -42,12 +41,18 @@ const SelectedDayList = ({
if (isPending) return <CenterLoader />
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 (
<Flex w='100%' h='100%' direction='column' align='center'>
<Text>{host.name} / {camera.name} / {data.day}</Text>
<DayAccordion recordSummary={data} />
<Text>{host.name} / {camera.name} / {stringDay}</Text>
<DayAccordion recordSummary={recordingsDay} />
</Flex>
);
};

View File

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