fix rerenders at events and days items
This commit is contained in:
parent
4100f731d1
commit
839cb90092
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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%' />
|
||||
|
||||
@ -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}`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
)
|
||||
|
||||
108
src/shared/components/accordion/DayAccordionItem.tsx
Normal file
108
src/shared/components/accordion/DayAccordionItem.tsx
Normal 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;
|
||||
52
src/shared/components/accordion/DayPanel.tsx
Normal file
52
src/shared/components/accordion/DayPanel.tsx
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
103
src/shared/components/accordion/EventsAccordionItem.tsx
Normal file
103
src/shared/components/accordion/EventsAccordionItem.tsx
Normal 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;
|
||||
@ -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} />
|
||||
:
|
||||
|
||||
@ -67,7 +67,7 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
|
||||
};
|
||||
}
|
||||
executed.current = true
|
||||
}, [videoUrl]);
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user