diff --git a/src/App.tsx b/src/App.tsx index 3d2f48b..bf1a9ce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,10 +54,17 @@ function App() { return auth.signinRedirect()} /> } + if (!auth.isAuthenticated && !auth.isLoading && authErrorCounter < maxErrorAuthConts) { - console.log('Not authenticated! Redirect! auth ErrorCounter', authErrorCounter) - setAuthErrorCounter(prevCount => prevCount + 1); - auth.signinRedirect() + if (hasAuthParams()) { + console.log('auth.isAuthenticated', auth.isAuthenticated) + console.log('auth.isLoading', auth.isLoading) + return 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) { diff --git a/src/pages/RecordingsPage.tsx b/src/pages/RecordingsPage.tsx index 166ed30..e944e4f 100644 --- a/src/pages/RecordingsPage.tsx +++ b/src/pages/RecordingsPage.tsx @@ -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('') const [cameraId, setCameraId] = useState('') 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 - const [startDay, endDay] = period if (startDay && endDay) { if (startDay.getDate() === endDay.getDate()) { // if select only one day diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 6792db4..a1c8eef 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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} /> ))} diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index 44a5196..c52a231 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -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(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('-') - const date = `${parts[0]}-${parts[1]}/${parts[2]}/${hour}` - return `${proxyPrefix}${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8` + 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}` diff --git a/src/services/frigate.proxy/frigate.schema.ts b/src/services/frigate.proxy/frigate.schema.ts index c17ed4d..d19fce7 100644 --- a/src/services/frigate.proxy/frigate.schema.ts +++ b/src/services/frigate.proxy/frigate.schema.ts @@ -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 export type PutConfig = z.infer export type GetFrigateHost = z.infer diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx index 662f53d..401013f 100644 --- a/src/shared/components/accordion/CameraAccordion.tsx +++ b/src/shared/components/accordion/CameraAccordion.tsx @@ -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
- if (isError) return - - if (!data || !camera) return null - const recodItem = (record: RecordSummary) => ( {strings.day}: {record.day} @@ -42,30 +37,37 @@ const CameraAccordion = () => { ) - const days = () => { - const [startDate, endDate] = recStore.selectedRange - if (startDate && endDate) { - return data - .filter(rec => { - const parsedRecDate = parseQueryDateToDate(rec.day) - if (parsedRecDate) { - return parsedRecDate >= startDate && parsedRecDate <= endDate - } - return false - }) - .map(rec => recodItem(rec)) - } - if ((startDate && endDate) || (!startDate && !endDate)) { - return data.map(rec => recodItem(rec)) + const days = useMemo(() => { + if (data && camera) { + const [startDate, endDate] = recStore.selectedRange + if (startDate && endDate) { + return data + .filter(rec => { + const parsedRecDate = parseQueryDateToDate(rec.day) + if (parsedRecDate) { + return parsedRecDate >= startDate && parsedRecDate <= endDate + } + return false + }) + .map(rec => recodItem(rec)) + } + if ((startDate && endDate) || (!startDate && !endDate)) { + return data.map(rec => recodItem(rec)) + } } return [] - } + }, [data, recStore.selectedRange]) + + if (isPending) return
+ if (isError) return + + if (!data || !camera) return null if (!isProduction) console.log('CameraAccordion rendered') return ( - {days()} + {days} ) } diff --git a/src/shared/components/accordion/DayAccordion.tsx b/src/shared/components/accordion/DayAccordion.tsx index b14aa02..e2176e7 100644 --- a/src/shared/components/accordion/DayAccordion.tsx +++ b/src/shared/components/accordion/DayAccordion.tsx @@ -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() const [openedValue, setOpenedValue] = useState() - const [playerUrl, setPlayerUrl] = useState() - 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 handleOpenPlayer = useCallback((value?: string) => { + if (recStore.playedItem !== value) { + setOpenedValue(value); + recStore.playedItem = value; + } else if (openedValue === value && recStore.playedItem === value) { + recStore.playedItem = undefined; } - 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(() => { - if (playedValue) { - const url = createRecordURL(playedValue) - if (url) { - if (!isProduction) console.log('GET URL: ', url) - setPlayerUrl(url) - } - } else { - setPlayerUrl(undefined) + const dayItems = useMemo(() => { + if (recordSummary && recordSummary.hours.length > 0) { + return recordSummary.hours.map(hour => { + const played = recordSummary.day + hour.hour === recStore.playedItem; + return ( + + ); + }); } - }, [playedValue]) + return []; + }, [recordSummary, hostName, camera, recStore.playedItem, handleOpenPlayer]) - if (!recordSummary || recordSummary.hours.length < 1) return Not have record at that day - - 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 Not have record at {recordSummary?.day} 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) => ( - - {strings.hour}: {hour}:00 - {eventsQty > 0 ? - {strings.events}: {eventsQty} - : - {strings.notHaveEvents} - } - - ) - - const hanleOpenNewLink = (recordId: string) => { - const link = createRecordURL(recordId) - if (link) { - const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}` - navigate(url) - } - } - return ( - {recordSummary.hours.map(hour => ( - - - - {hourLabel(hour.hour, hour.events)} - - - hanleOpenNewLink(hour.hour)}> - - - - - - - - {playedValue === hour.hour && playerUrl ? : <>} - { } - {recStore.filteredHost && camera && hostName ? - - - - : ''} - {hour.events > 0 ? - - : -
{strings.notHaveEvents}
- } -
-
- ))} - + {dayItems}
) diff --git a/src/shared/components/accordion/DayAccordionItem.tsx b/src/shared/components/accordion/DayAccordionItem.tsx new file mode 100644 index 0000000..67d211f --- /dev/null +++ b/src/shared/components/accordion/DayAccordionItem.tsx @@ -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() + + 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 ( + + + + + {strings.hour}: {hour}:00 + {recordHour.events > 0 ? + {strings.events}: {recordHour.events} + : + {strings.notHaveEvents} + } + + + + + + + + + + + + + ); +}; + +export default DayAccordionItem; \ No newline at end of file diff --git a/src/shared/components/accordion/DayPanel.tsx b/src/shared/components/accordion/DayPanel.tsx new file mode 100644 index 0000000..f8f11ab --- /dev/null +++ b/src/shared/components/accordion/DayPanel.tsx @@ -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 ( + + {playedURL && playedURL === videoURL ? : <>} + {cameraName && hostName ? + + + + : ''} + {events > 0 ? + + : +
{strings.notHaveEvents}
+ } +
+ ); +}; + +export default DayPanel; \ No newline at end of file diff --git a/src/shared/components/accordion/EventPanel.tsx b/src/shared/components/accordion/EventPanel.tsx index da5108a..47cb5cd 100644 --- a/src/shared/components/accordion/EventPanel.tsx +++ b/src/shared/components/accordion/EventPanel.tsx @@ -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 ? : <>} + {playedURL && playedURL === videoURL ? : <>} {!hostName ? <> : { const { recordingsStore: recStore } = useContext(Context) - const [playedValue, setPlayedValue] = useState() const [openedItem, setOpenedItem] = useState() - const [playerUrl, setPlayerUrl] = useState() - 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
if (isError) return if (!data || data.length < 1) return
Not have events at that period
- 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 ( - - {strings.player.object}: {event.label} - {time} - {duration ? - {duration} - : <>} - - ) - } - - 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 ( {data.map(event => ( - - - - {eventLabel(event)} - - - hanleOpenNewLink(event.id)}> - - - - - - - - - - + ))} ); diff --git a/src/shared/components/accordion/EventsAccordionItem.tsx b/src/shared/components/accordion/EventsAccordionItem.tsx new file mode 100644 index 0000000..1b73df6 --- /dev/null +++ b/src/shared/components/accordion/EventsAccordionItem.tsx @@ -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() + + 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 ( + + {strings.player.object}: {event.label} + {time} + {duration ? + {duration} + : <>} + + ) + } + + 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 ( + + + + {eventLabel(event)} + + + + + + + + + + + + + + ); +}; + +export default EventsAccordionItem; \ No newline at end of file diff --git a/src/shared/components/buttons/PlayControl.tsx b/src/shared/components/buttons/PlayControl.tsx index 296c376..af5ec90 100644 --- a/src/shared/components/buttons/PlayControl.tsx +++ b/src/shared/components/buttons/PlayControl.tsx @@ -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 ( { handleClick(value) }} + onClick={() => { handleClick() }} > - {playedValue === value ? strings.player.stopVideo : strings.player.startVideo} - {playedValue === value ? + {played ? strings.player.stopVideo : strings.player.startVideo} + {played ? : diff --git a/src/shared/components/players/VideoPlayer.tsx b/src/shared/components/players/VideoPlayer.tsx index 24b3727..594ca40 100644 --- a/src/shared/components/players/VideoPlayer.tsx +++ b/src/shared/components/players/VideoPlayer.tsx @@ -67,7 +67,7 @@ const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => { }; } executed.current = true - }, [videoUrl]); + }, []); useEffect(() => { diff --git a/src/shared/stores/recordings.store.ts b/src/shared/stores/recordings.store.ts index 76dbb1f..5d5bf02 100644 --- a/src/shared/stores/recordings.store.ts +++ b/src/shared/stores/recordings.store.ts @@ -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 diff --git a/src/widgets/SelectedDayList.tsx b/src/widgets/SelectedDayList.tsx index 71272db..d250d1d 100644 --- a/src/widgets/SelectedDayList.tsx +++ b/src/widgets/SelectedDayList.tsx @@ -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 if (isError) return - if (!camera || !host || !data) return + if (!camera || !host) return + if (!data) return Not have response from server + const stringDay = dateToQueryString(day) + const recordingsDay = data.find(record => record.day === stringDay) + if (!recordingsDay) return
Not have record at {stringDay}
+ + if (!isProduction) console.log('SelectedDayList rendered') return ( - {host.name} / {camera.name} / {data.day} - + {host.name} / {camera.name} / {stringDay} + ); }; diff --git a/src/widgets/VideoDownloader.tsx b/src/widgets/VideoDownloader.tsx index e66bfad..ce22344 100644 --- a/src/widgets/VideoDownloader.tsx +++ b/src/widgets/VideoDownloader.tsx @@ -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