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()} />
|
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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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%' />
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
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 { 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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
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, 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} />
|
||||||
:
|
:
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
executed.current = true
|
executed.current = true
|
||||||
}, [videoUrl]);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user