diff --git a/src/AppBody.tsx b/src/AppBody.tsx index f85ab2b..a22a46c 100644 --- a/src/AppBody.tsx +++ b/src/AppBody.tsx @@ -3,9 +3,9 @@ import { AppShell, useMantineTheme, } from "@mantine/core" import { HeaderAction } from './widgets/header/HeaderAction'; import { testHeaderLinks } from './widgets/header/header.links'; import AppRouter from './router/AppRouter'; -import { SideBar } from './shared/components/SideBar'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Context } from '.'; +import SideBar from './shared/components/SideBar'; const queryClient = new QueryClient() diff --git a/src/pages/RecordingsPage.tsx b/src/pages/RecordingsPage.tsx index 27a6d64..2573105 100644 --- a/src/pages/RecordingsPage.tsx +++ b/src/pages/RecordingsPage.tsx @@ -1,1072 +1,82 @@ -import MultiSelect from '../shared/components/frigate/MultiSelect'; -import StarRecording from '../shared/components/frigate/icons/StarRecording'; -import Submitted from '../shared/components/frigate/icons/Submitted'; -import CalendarIcon from '../shared/components/frigate/icons/CalendarIcon'; -import Menu, { MenuItem } from '../shared/components/frigate/Menu'; -import Dialog from '../shared/components/frigate/Dialog'; -import TimelineSummary from '../shared/components/frigate/TimelineSummary'; -import TimelineEventOverlay from '../shared/components/frigate/TimelineEventOverlay'; -import { Tabs, TextTab } from '../shared/components/frigate/Tabs'; -import Clock from '../shared/components/frigate/icons/Clock'; -import TimeAgo from '../shared/components/frigate/TimeAgo'; -import Camera from '../shared/components/frigate/icons/Camera'; -import Zone from '../shared/components/frigate/icons/Zone'; -import Score from '../shared/components/frigate/icons/Score'; -import Link from '../shared/components/frigate/Link'; -import Delete from '../shared/components/frigate/icons/Delete'; -import Download from '../shared/components/frigate/icons/Download'; -import { IconDownload, IconStar, IconStarFilled } from '@tabler/icons-react'; -// ↑↑↑ from frigate ↑↑↑ -import { Fragment, useState, useRef, useCallback, useMemo, useContext, useEffect, lazy, Suspense } from 'react'; -import VideoPlayer from '../shared/components/frigate/VideoPlayer'; -import CogwheelLoader from '../shared/components/CogwheelLoader'; -import { Accordion, Button, Center, Flex, Grid, SelectItem, Text } from '@mantine/core'; -import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; -import { useLocation, useParams } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; +import { useState, useContext, useEffect, lazy, Suspense } from 'react'; +import { Accordion, Flex, Text } from '@mantine/core'; +import { useLocation, useNavigate } from 'react-router-dom'; import { observer } from 'mobx-react-lite'; import { Context } from '..'; -import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter'; -import CenterLoader from '../shared/components/CenterLoader'; -import RetryError from './RetryError'; -const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion')); +import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide'; +import SelectedCameraList from '../widgets/SelectedCameraList'; +import SelectedHostList from '../widgets/SelectedHostList'; +import { useQuery } from '@tanstack/react-query'; +import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; +const recordingsQuery = { + hostId: 'hostId', + cameraId: 'cameraId', + date: 'date', + hour: 'hour', +} + const RecordingsPage = observer(() => { - const { sideBarsStore } = useContext(Context) - const filterData: OneSelectItem[] = [ - { value: 'dsfgdfg', label: 'fasdfsdf' }, - { value: 'dsfgsfgnjcv', label: 'frteh' }, - { value: 'rthsdfgh', label: 'dftghdfgjn' }, - ] - const hostSelector = () => ( - - ) - useEffect(() => { - sideBarsStore.rightVisible = true - sideBarsStore.setRightChildren(hostSelector()) - }, []) - - + const { sideBarsStore, recordingsStore: recStore } = useContext(Context) const location = useLocation() + const navigate = useNavigate() const queryParams = new URLSearchParams(location.search) - const cameraId = queryParams.get('cameraId'); - const hostId = queryParams.get('hostId') - const date = queryParams.get('date'); - const time = queryParams.get('time'); + const paramHostId = queryParams.get(recordingsQuery.hostId) + const paramCameraId = queryParams.get(recordingsQuery.cameraId); + const paramDate = queryParams.get(recordingsQuery.date); + const paramTime = queryParams.get(recordingsQuery.hour); - const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({ - queryKey: [frigateQueryKeys.getCameraWHost, cameraId], - queryFn: async () => { - if (cameraId) { - return frigateApi.getCameraWHost(cameraId) - } - return null - } - }) - const { data: host, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({ - queryKey: [frigateQueryKeys.getFrigateHost, hostId], - queryFn: async () => { - if (hostId) { - return frigateApi.getHost(hostId) - } - return null - } - }) + const [hostId, setHostId] = useState('') + const [cameraId, setCameraId] = useState('') - const [openCameraId, setOpenCameraId] = useState(null) - - const handleOnChange = (cameraId: string | null) => { - console.log('Camera id', cameraId) - setOpenCameraId(openCameraId === cameraId ? null : cameraId) - } - - const handleRetry = () => { - if (cameraId) cameraRefetch() - else if (hostId) hostRefetch() - } - - if (hostPending || cameraPending) return - if (hostError || cameraError) return - - - // Camera selected - if (camera && camera.frigateHost) { - return ( - - {camera.frigateHost.name} - - - - + useEffect(() => { + sideBarsStore.rightVisible = true + sideBarsStore.setRightChildren( + ) - } - // Host selected - if (host && host.cameras.length > 0) { + if (paramHostId) recStore.hostIdParam = paramHostId + if (paramCameraId) recStore.cameraIdParam = paramCameraId + return () => sideBarsStore.setRightChildren(null) + }, []) - const cameras = host.cameras.map(camera => { - return ( - - {camera.name} - - {openCameraId === camera.id && ( - - - - )} - - - ) - }) + useEffect(() => { + setHostId(recStore.selectedHost?.id || '') + if (recStore.selectedHost) { + queryParams.set(recordingsQuery.hostId, recStore.selectedHost.id) + } else { + queryParams.delete(recordingsQuery.hostId) + } + navigate({ pathname: location.pathname, search: queryParams.toString() }); + }, [recStore.selectedHost]) - return ( - - {host.name} - handleOnChange(value)}> - {cameras} - - - ) + useEffect(() => { + setCameraId(recStore.selectedCamera?.id || '') + if (recStore.selectedCamera) { + queryParams.set(recordingsQuery.cameraId, recStore.selectedCamera?.id) + } else { + console.log('delete recordingsQuery.cameraId') + queryParams.delete(recordingsQuery.cameraId) + } + navigate({ pathname: location.pathname, search: queryParams.toString() }); + }, [recStore.selectedCamera]) + + if (cameraId) { + return } + if (hostId) { + return + } + + console.log('RecordingsPage rendered') return ( Please select host ) - - - // const videoUrl = proxyApi.eventURL('localhost:5000', event) - // const recordUrl = 'http://127.0.0.1:5000/vod/2024-02/22/18/Buhgalteria/Asia,Krasnoyarsk/master.m3u8' - // const recordUrl = proxyApi.recordingURL('localhost:5000', 'Buhgalteria', 'Asia,Krasnoyarsk', '2024-02/22/18') - // console.log(recordUrl) - - // return ( - // - // {/* */} - // - // ) - - // seekOptions={{ forward: 10, backward: 5 }} - // onReady={handlePlayerReady} - // onDispose={onDispose} - {/* {eventOverlay ? ( - - ) : null} */} - return ( -
- ) }) -export default RecordingsPage - -// const API_LIMIT = 25; - -// const daysAgo = (num: number) => { -// let date = new Date(); -// date.setDate(date.getDate() - num); -// return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000; -// }; - -// const monthsAgo = (num: number) => { -// let date = new Date(); -// date.setMonth(date.getMonth() - num); -// return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000; -// }; - -// export default function Events({ path, ...props }) { -// // const apiHost = useApiHost(); -// // const { data: config } = useSWR('config'); -// const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone -// const [searchParams, setSearchParams] = useState({ -// before: null, -// after: null, -// cameras: props.cameras ?? 'all', -// labels: props.labels ?? 'all', -// zones: props.zones ?? 'all', -// sub_labels: props.sub_labels ?? 'all', -// time_range: '00:00,24:00', -// timezone, -// favorites: props.favorites ?? 0, -// is_submitted: props.is_submitted ?? -1, -// event: props.event, -// }); -// const [state, setState] = useState({ -// showDownloadMenu: false, -// showDatePicker: false, -// showCalendar: false, -// showPlusSubmit: false, -// }); -// const [plusSubmitEvent, setPlusSubmitEvent] = useState({ -// id: null, -// label: null, -// validBox: null, -// }); -// const [uploading, setUploading] = useState([]); -// const [viewEvent, setViewEvent] = useState(props.event); -// const [eventOverlay, setEventOverlay] = useState(); -// const [eventDetailType, setEventDetailType] = useState('clip'); -// const [downloadEvent, setDownloadEvent] = useState({ -// id: null, -// label: null, -// box: null, -// has_clip: false, -// has_snapshot: false, -// plus_id: undefined, -// end_time: null, -// }); -// const [deleteFavoriteState, setDeleteFavoriteState] = useState({ -// deletingFavoriteEventId: null, -// showDeleteFavorite: false, -// }); - -// const [showInProgress, setShowInProgress] = useState((props.event || props.cameras || props.labels) == null); - -// const eventsFetcher = useCallback( -// (path, params) => { -// if (searchParams.event) { -// path = `${path}/${searchParams.event}`; -// return axios.get(path).then((res) => [res.data]); -// } -// params = { ...params, in_progress: 0, include_thumbnails: 0, limit: API_LIMIT }; -// return axios.get(path, { params }).then((res) => res.data); -// }, -// [searchParams] -// ); - -// const getKey = useCallback( -// (index, prevData) => { -// if (index > 0) { -// const lastDate = prevData[prevData.length - 1].start_time; -// const pagedParams = { ...searchParams, before: lastDate }; -// return ['events', pagedParams]; -// } - -// return ['events', searchParams]; -// }, -// [searchParams] -// ); - -// const { data: ongoingEvents, mutate: refreshOngoingEvents } = useSWR([ -// 'events', -// { in_progress: 1, include_thumbnails: 0 }, -// ]); -// // const { -// // data: eventPages, -// // mutate: refreshEvents, -// // size, -// // setSize, -// // isValidating, -// // } = useSWRInfinite(getKey, eventsFetcher); -// // const mutate = () => { -// // refreshEvents(); -// // refreshOngoingEvents(); -// // }; - -// // const { data: allLabels } = useSWR(['labels']); -// // const { data: allSubLabels } = useSWR(['sub_labels', { split_joined: 1 }]); - -// const filterValues = useMemo( -// () => ({ -// cameras: Object.keys(config?.cameras || {}), -// zones: [ -// ...Object.values(config?.cameras || {}) -// .reduce((memo, camera) => { -// memo = memo.concat(Object.keys(camera?.zones || {})); -// return memo; -// }, []) -// .filter((value, i, self) => self.indexOf(value) === i), -// 'None', -// ], -// labels: Object.values(allLabels || {}), -// sub_labels: (allSubLabels || []).length > 0 ? [...Object.values(allSubLabels), 'None'] : [], -// }), -// [config, allLabels, allSubLabels] -// ); - -// const onSave = async (e, eventId, save) => { -// e.stopPropagation(); -// let response; -// if (save) { -// response = await axios.post(`events/${eventId}/retain`); -// } else { -// response = await axios.delete(`events/${eventId}/retain`); -// } -// if (response.status === 200) { -// mutate(); -// } -// }; - -// const onDelete = async (e, eventId, saved) => { -// e.stopPropagation(); - -// if (saved) { -// setDeleteFavoriteState({ deletingFavoriteEventId: eventId, showDeleteFavorite: true }); -// } else { -// const response = await axios.delete(`events/${eventId}`); -// if (response.status === 200) { -// mutate(); -// } -// } -// }; - -// const onToggleNamedFilter = (name, item) => { -// let items; - -// if (searchParams[name] == 'all') { -// const currentItems = Array.from(filterValues[name]); - -// // don't remove all if only one option -// if (currentItems.length > 1) { -// currentItems.splice(currentItems.indexOf(item), 1); -// items = currentItems.join(','); -// } else { -// items = ['all']; -// } -// } else { -// let currentItems = searchParams[name].length > 0 ? searchParams[name].split(',') : []; - -// if (currentItems.includes(item)) { -// // don't remove the last item in the filter list -// if (currentItems.length > 1) { -// currentItems.splice(currentItems.indexOf(item), 1); -// } - -// items = currentItems.join(','); -// } else if (currentItems.length + 1 == filterValues[name].length) { -// items = ['all']; -// } else { -// currentItems.push(item); -// items = currentItems.join(','); -// } -// } - -// onFilter(name, items); -// }; - -// const onEventFrameSelected = (event, frame, seekSeconds) => { -// if (this.player) { -// this.player.pause(); -// this.player.currentTime(seekSeconds); -// setEventOverlay(frame); -// } -// }; - -// const datePicker = useRef(); - -// const downloadButton = useRef(); - -// const onDownloadClick = (e, event) => { -// e.stopPropagation(); -// setDownloadEvent((_prev) => ({ -// id: event.id, -// box: event?.data?.box || event.box, -// label: event.label, -// has_clip: event.has_clip, -// has_snapshot: event.has_snapshot, -// plus_id: event.plus_id, -// end_time: event.end_time, -// })); -// downloadButton.current = e.target; -// setState({ ...state, showDownloadMenu: true }); -// }; - -// const showSubmitToPlus = (event_id, label, box, e) => { -// if (e) { -// e.stopPropagation(); -// } -// // if any of the box coordinates are > 1, then the box data is from an older version -// // and not valid to submit to plus with the snapshot image -// setPlusSubmitEvent({ id: event_id, label, validBox: !box.some((d) => d > 1) }); -// setState({ ...state, showDownloadMenu: false, showPlusSubmit: true }); -// }; - -// const handleSelectDateRange = useCallback( -// (dates) => { -// setShowInProgress(false); -// setSearchParams({ ...searchParams, before: dates.before, after: dates.after }); -// setState({ ...state, showDatePicker: false }); -// }, -// [searchParams, setSearchParams, state, setState] -// ); - -// const handleSelectTimeRange = useCallback( -// (timeRange) => { -// setSearchParams({ ...searchParams, time_range: timeRange }); -// }, -// [searchParams] -// ); - -// const onFilter = useCallback( -// (name, value) => { -// setShowInProgress(false); -// const updatedParams = { ...searchParams, [name]: value }; -// setSearchParams(updatedParams); -// const queryString = Object.keys(updatedParams) -// .map((key) => { -// if (updatedParams[key] && updatedParams[key] != 'all') { -// return `${key}=${updatedParams[key]}`; -// } -// return null; -// }) -// .filter((val) => val) -// .join('&'); -// route(`${path}?${queryString}`); -// }, -// [path, searchParams, setSearchParams] -// ); - -// // const onClickFilterSubmitted = useCallback(() => { -// // if (++searchParams.is_submitted > 1) { -// // searchParams.is_submitted = -1; -// // } -// // onFilter('is_submitted', searchParams.is_submitted); -// // }, [searchParams, onFilter]); - -// const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT; - -// // hooks for infinite scroll -// const observer = useRef(); -// const lastEventRef = useCallback( -// (node) => { -// if (isValidating) return; -// if (observer.current) observer.current.disconnect(); -// try { -// observer.current = new IntersectionObserver((entries) => { -// if (entries[0].isIntersecting && !isDone) { -// setSize(size + 1); -// } -// }); -// if (node) observer.current.observe(node); -// } catch (e) { -// // no op -// } -// }, -// [size, setSize, isValidating, isDone] -// ); - -// const onSendToPlus = async (id, false_positive, validBox) => { -// if (uploading.includes(id)) { -// return; -// } - -// setUploading((prev) => [...prev, id]); - -// const response = false_positive -// ? await axios.put(`events/${id}/false_positive`) -// : await axios.post(`events/${id}/plus`, validBox ? { include_annotation: 1 } : {}); - -// if (response.status === 200) { -// mutate( -// (pages) => -// pages.map((page) => -// page.map((event) => { -// if (event.id === id) { -// return { ...event, plus_id: response.data.plus_id }; -// } -// return event; -// }) -// ), -// false -// ); -// } - -// setUploading((prev) => prev.filter((i) => i !== id)); - -// if (state.showDownloadMenu && downloadEvent.id === id) { -// setState({ ...state, showDownloadMenu: false }); -// } - -// setState({ ...state, showPlusSubmit: false }); -// }; - -// const handleEventDetailTabChange = (index) => { -// setEventDetailType(index == 0 ? 'clip' : 'image'); -// }; - -// if (!config) { -// return ; -// } - -// return ( -//
-// Events -//
-// onToggleNamedFilter('cameras', item)} -// onShowAll={() => onFilter('cameras', ['all'])} -// onSelectSingle={(item) => onFilter('cameras', item)} -// /> -// onToggleNamedFilter('labels', item)} -// onShowAll={() => onFilter('labels', ['all'])} -// onSelectSingle={(item) => onFilter('labels', item)} -// /> -// onToggleNamedFilter('zones', item)} -// onShowAll={() => onFilter('zones', ['all'])} -// onSelectSingle={(item) => onFilter('zones', item)} -// /> -// {filterValues.sub_labels.length > 0 && ( -// onToggleNamedFilter('sub_labels', item)} -// onShowAll={() => onFilter('sub_labels', ['all'])} -// onSelectSingle={(item) => onFilter('sub_labels', item)} -// /> -// )} -// {searchParams.event && ( -// -// )} - -//
-// {/* {config.plus.enabled && ( -// onClickFilterSubmitted()} -// inner_fill={searchParams.is_submitted == 1 ? 'currentColor' : 'gray'} -// outer_stroke={searchParams.is_submitted >= 0 ? 'currentColor' : 'gray'} -// /> -// )} */} - -// onFilter('favorites', searchParams.favorites ? 0 : 1)} -// fill={searchParams.favorites == 1 ? 'currentColor' : 'none'} -// /> -//
- -//
-// setState({ ...state, showDatePicker: true })} -// /> -//
-//
-// {state.showDownloadMenu && ( -// setState({ ...state, showDownloadMenu: false })} relativeTo={downloadButton}> -// {downloadEvent.has_snapshot && ( -// -// )} -// {downloadEvent.has_clip && ( -// -// )} -// {(event?.data?.type || 'object') == 'object' && -// downloadEvent.end_time && -// downloadEvent.has_snapshot && -// !downloadEvent.plus_id && ( -// showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)} -// /> -// )} -// {downloadEvent.plus_id && ( -// setState({ ...state, showDownloadMenu: false })} -// /> -// )} -// -// )} -// {state.showDatePicker && ( -// setState({ ...state, setShowDatePicker: false })} -// relativeTo={datePicker} -// > -// -// -// -// -// -// -// { -// setState({ ...state, showCalendar: true, showDatePicker: false }); -// }} -// /> -// -// )} - -// {state.showCalendar && ( -// -// setState({ ...state, showCalendar: false })} -// relativeTo={datePicker} -// > -// setState({ ...state, showCalendar: false })} -// > -// -// -// -// -// )} -// {state.showPlusSubmit && ( -// -// {config.plus.enabled ? ( -// <> -//
-// Submit to Frigate+ - -// {`${plusSubmitEvent.label}`} - -// {plusSubmitEvent.validBox ? ( -//

-// Objects in locations you want to avoid are not false positives. Submitting them as false positives -// will confuse the model. -//

-// ) : ( -//

-// Events prior to version 0.13 can only be submitted to Frigate+ without annotations. -//

-// )} -//
-// {plusSubmitEvent.validBox ? ( -//
-// -// -// -//
-// ) : ( -//
-// -// -//
-// )} -// -// ) : ( -// <> -//
-// Setup a Frigate+ Account -//

In order to submit images to Frigate+, you first need to setup an account.

-// -// https://plus.frigate.video -// -//
-//
-// -//
-// -// )} -//
-// )} -// {deleteFavoriteState.showDeleteFavorite && ( -// -//
-// Delete Saved Event? -//

Confirm deletion of saved event.

-//
-//
-// -// -//
-//
-// )} -//
-// {ongoingEvents ? ( -//
-//
-// -// Ongoing Events -// -// -// -//
-// {showInProgress && -// ongoingEvents.map((event, _) => { -// return ( -// { -// this.player = null; -// }} -// onDownloadClick={onDownloadClick} -// onReady={(player) => { -// this.player = player; -// this.player.on('playing', () => { -// setEventOverlay(undefined); -// }); -// }} -// onSave={onSave} -// showSubmitToPlus={showSubmitToPlus} -// /> -// ); -// })} -//
-// ) : null} -// -// Past Events -// -// {eventPages ? ( -// eventPages.map((page, i) => { -// const lastPage = eventPages.length === i + 1; -// return page.map((event, j) => { -// const lastEvent = lastPage && page.length === j + 1; -// return ( -// { -// this.player = null; -// }} -// onDownloadClick={onDownloadClick} -// onReady={(player) => { -// this.player = player; -// this.player.on('playing', () => { -// setEventOverlay(undefined); -// }); -// }} -// onSave={onSave} -// showSubmitToPlus={showSubmitToPlus} -// /> -// ); -// }); -// }) -// ) : ( -// -// )} -//
-//
{isDone ? null : }
-//
-// ); -// } - -// function Event({ -// className = '', -// config, -// event, -// eventDetailType, -// eventOverlay, -// viewEvent, -// setViewEvent, -// lastEvent, -// lastEventRef, -// uploading, -// handleEventDetailTabChange, -// onEventFrameSelected, -// onDelete, -// onDispose, -// onDownloadClick, -// onReady, -// onSave, -// showSubmitToPlus, -// }) { -// const apiHost = useApiHost(); - -// return ( -//
-//
(viewEvent === event.id ? setViewEvent(null) : setViewEvent(event.id))} -// > -//
-// onSave(e, event.id, !event.retain_indefinitely)} -// fill={event.retain_indefinitely ? 'yellow' : 'none'} /> -// {/* onSave(e, event.id, !event.retain_indefinitely)} -// fill={event.retain_indefinitely ? 'currentColor' : 'none'} -// /> */} -// {event.end_time ? null : ( -//
-// In progress -//
-// )} -//
-//
-//
-//
-// {event.label.replaceAll('_', ' ')} -// {event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null} -//
- -//
-// -// {formatUnixTimestampToDateTime(event.start_time, { ...config.ui })} -//
-// - -// -//
-//
-// ( {getDurationFromTimestamps(event.start_time, event.end_time)} ) -//
-//
-//
-// -// {event.camera.replaceAll('_', ' ')} -//
-// {event.zones.length ? ( -//
-// -// {event.zones.join(', ').replaceAll('_', ' ')} -//
-// ) : null} -//
-// -// {(event?.data?.top_score || event.top_score || 0) == 0 -// ? null -// : `${event.label}: ${((event?.data?.top_score || event.top_score) * 100).toFixed(0)}%`} -// {(event?.data?.sub_label_score || 0) == 0 -// ? null -// : `, ${event.sub_label}: ${(event?.data?.sub_label_score * 100).toFixed(0)}%`} -//
-//
-// {/* */} -//
-// {event.has_clip || event.has_snapshot ? -// onDownloadClick(e, event)} /> -// : <>} -//
-//
-//
-// {viewEvent !== event.id ? null : ( -//
-//
-//
-// -// -// -// -//
- -//
-// {eventDetailType == 'clip' && event.has_clip ? ( -//
-// onEventFrameSelected(event, frame, seekSeconds)} -// /> -//
-// -// {eventOverlay ? ( -// -// ) : null} -// -//
-//
-// ) : null} - -// {eventDetailType == 'image' || !event.has_clip ? ( -//
-// {`${event.label} -//
-// ) : null} -//
-//
-//
-// )} -//
-// ); -// } +export default RecordingsPage \ No newline at end of file diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index 13aa183..ed5e2f8 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -5,6 +5,7 @@ import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost import { FrigateConfig } from "../../types/frigateConfig"; import { url } from "inspector"; import { RecordSummary } from "../../types/record"; +import { EventFrigate } from "../../types/event"; const instanceApi = axios.create({ @@ -59,25 +60,32 @@ export const proxyApi = { cameraName: string, timezone: string, ) => - instanceApi.get(`proxy/${hostName}/api/${cameraName}/recordings/summary`, {params: { timezone}}).then(res => res.data), + instanceApi.get(`proxy/${hostName}/api/${cameraName}/recordings/summary`, { params: { timezone } }).then(res => res.data), + // E.g. http://127.0.0.1:5000/api/events?before=1708534799&after=1708448400&camera=CameraName&has_clip=1&include_thumbnails=0&limit=5000 getEvents: ( hostName: string, camerasName: string[], - timezone: string, - minScore?: number, - maxScore?: number, + timezone?: string, + hasClip?: boolean, after?: number, before?: number, labels?: string[], + limit: number = 5000, + includeThumnails?: boolean, + minScore?: number, + maxScore?: number, ) => - instanceApi.get(`proxy/${hostName}/api/events`, { + instanceApi.get(`proxy/${hostName}/api/events`, { params: { - cameras: camerasName, - after: after, + cameras: camerasName.join(','), // frigate format timezone: timezone, + after: after, before: before, // @before the last event start_time in list + has_clip: hasClip, + include_thumbnails: includeThumnails, labels: labels, + limit: limit, min_score: minScore, max_score: maxScore, } @@ -120,4 +128,5 @@ export const frigateQueryKeys = { getHostConfig: 'host-config', getRecordingsSummary: 'recordings-frigate-summary', getRecordings: 'recordings-frigate', + getEvents: 'events-frigate', } diff --git a/src/services/frigate.proxy/frigate.schema.ts b/src/services/frigate.proxy/frigate.schema.ts index 814c5d1..c2affad 100644 --- a/src/services/frigate.proxy/frigate.schema.ts +++ b/src/services/frigate.proxy/frigate.schema.ts @@ -58,6 +58,20 @@ export const deleteFrigateHostSchema = putFrigateHostSchema.pick({ id: true, }); +export const getEventsQuerySchema = z.object({ + hostName: z.string(), + camerasName: z.string().array(), + timezone: z.string().optional(), + hasClip: z.boolean().optional(), + after: z.number().optional(), + before: z.number().optional(), + labels: z.string().array().optional(), + limit: z.number().optional(), + includeThumnails: z.boolean().optional(), + minScore: z.number().optional(), + maxScore: z.number().optional(), +}) + export type GetConfig = z.infer export type PutConfig = z.infer export type GetFrigateHost = z.infer diff --git a/src/shared/components/SideBar.tsx b/src/shared/components/SideBar.tsx index 125b6f1..ddb3795 100644 --- a/src/shared/components/SideBar.tsx +++ b/src/shared/components/SideBar.tsx @@ -28,7 +28,7 @@ const useStyles = createStyles((theme, })) -export const SideBar = observer(({ isHidden, side, children }: SideBarProps) => { +const SideBar = ({ isHidden, side, children }: SideBarProps) => { const hideSizePx = useMantineSize(dimensions.hideSidebarsSize) const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx); const manualVisible: React.MutableRefObject = useRef(null) @@ -43,7 +43,7 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) => const { sideBarsStore } = useContext(Context) - useEffect( () => { + useEffect(() => { if (sideBarsStore.rightVisible && side === 'right' && !visible) { open() } else if (!sideBarsStore.rightVisible && side === 'right' && visible) { @@ -117,5 +117,6 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) => handleClickVisible(true)} />
) -}) +} +export default observer(SideBar) \ No newline at end of file diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx index c708365..8f1ca5a 100644 --- a/src/shared/components/accordion/CameraAccordion.tsx +++ b/src/shared/components/accordion/CameraAccordion.tsx @@ -3,10 +3,10 @@ import React, { useContext, useEffect, useState } from 'react'; import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema'; import { useQuery } from '@tanstack/react-query'; import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; -import CogwheelLoader from '../CogwheelLoader'; import DayAccordion from './DayAccordion'; import { observer } from 'mobx-react-lite'; import { Context } from '../../..'; +import { getResolvedTimeZone } from '../frigate/dateUtil'; interface CameraAccordionProps { camera: GetCameraWHostWConfig, @@ -17,14 +17,14 @@ const CameraAccordion = observer(({ camera, host }: CameraAccordionProps) => { - const { recordingsStore } = useContext(Context) + const { recordingsStore: recStore } = useContext(Context) const { data, isPending, isError } = useQuery({ queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id], queryFn: () => { if (camera && host) { const hostName = mapHostToHostname(host) - return proxyApi.getRecordingsSummary(hostName, camera.name, 'Asia/Krasnoyarsk') + return proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone()) } return null } @@ -34,26 +34,23 @@ const CameraAccordion = observer(({ useEffect(() => { if (openedDay) { - recordingsStore.playedRecord.cameraName = camera.name - const hostName = mapHostToHostname(host) - recordingsStore.playedRecord.hostName = hostName + recStore.recordToPlay.cameraName = camera.name + const hostName = mapHostToHostname(host) + recStore.recordToPlay.hostName = hostName } - }, [openedDay]) + }, [openedDay]) const handleClick = (value: string | null) => { setOpenedDay(value) } - if (isPending) return ( -
- Loading... -
- ) - if (isError) return Loading error + if (isPending) return
Loading...
+ if (isError) return
Loading error
if (!data || !camera) return null - const days = data.map(rec => ( + + const days = data.slice(0, 2).map(rec => ( {rec.day} @@ -62,6 +59,9 @@ const CameraAccordion = observer(({ )) + + console.log('CameraAccordion rendered') + return ( {days} diff --git a/src/shared/components/accordion/DayAccordion.tsx b/src/shared/components/accordion/DayAccordion.tsx index 783165e..c29c330 100644 --- a/src/shared/components/accordion/DayAccordion.tsx +++ b/src/shared/components/accordion/DayAccordion.tsx @@ -1,4 +1,4 @@ -import { Accordion, Flex, Group, Text } from '@mantine/core'; +import { Accordion, Center, Flex, Group, Text } from '@mantine/core'; import React, { useContext, useEffect, useState } from 'react'; import { RecordHour, RecordSummary, Recording } from '../../../types/record'; import Button from '../frigate/Button'; @@ -8,6 +8,9 @@ import PlayControl from './PlayControl'; import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { Context } from '../../..'; import VideoPlayer from '../frigate/VideoPlayer'; +import { getResolvedTimeZone } from '../frigate/dateUtil'; +import EventsAccordion from './EventsAccordion'; +import DayEventsAccordion from './DayEventsAccordion'; interface RecordingAccordionProps { recordSummary?: RecordSummary @@ -25,11 +28,11 @@ const DayAccordion = observer(({ if (openVideoPlayer) { console.log('openVideoPlayer', openVideoPlayer) if (openVideoPlayer) { - recordingsStore.playedRecord.day = recordSummary?.day - recordingsStore.playedRecord.hour = openVideoPlayer - recordingsStore.playedRecord.timezone = 'Asia,Krasnoyarsk' - const parsed = recordingsStore.getFullPlayedRecord(recordingsStore.playedRecord) - console.log('recordingsStore.playedRecord: ', recordingsStore.playedRecord) + recordingsStore.recordToPlay.day = recordSummary?.day + recordingsStore.recordToPlay.hour = openVideoPlayer + recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',') + const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay) + console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay) if (parsed.success) { const url = proxyApi.recordingURL( parsed.data.hostName, @@ -42,7 +45,7 @@ const DayAccordion = observer(({ setPlayerUrl(url) } } - }else { + } else { setPlayerUrl(undefined) } }, [openVideoPlayer]) @@ -68,6 +71,8 @@ const DayAccordion = observer(({ setOpenVideoPlayer(undefined) } + console.log('DayAccordion rendered') + return ( - {recordSummary.hours.map(hour => ( + {recordSummary.hours.slice(0, 5).map(hour => ( - + - {openVideoPlayer === hour.hour ? : <>} - Events + {openVideoPlayer === hour.hour && playerUrl ? : <>} + {hour.events > 0 ? + + : +
Not have events
+ }
))} diff --git a/src/shared/components/accordion/DayEventsAccordion.tsx b/src/shared/components/accordion/DayEventsAccordion.tsx new file mode 100644 index 0000000..05ea739 --- /dev/null +++ b/src/shared/components/accordion/DayEventsAccordion.tsx @@ -0,0 +1,38 @@ +import { Accordion, Text } from '@mantine/core'; +import React, { Suspense, lazy, useState } from 'react'; +const EventsAccordion = lazy(() => import('./EventsAccordion')) + +interface DayEventsAccordionProps { + day: string, + hour: string, + qty?: number, +} + +const DayEventsAccordion = ({ + day, + hour, + qty, +}: DayEventsAccordionProps) => { + const [openedItem, setOpenedItem] = useState() + + const handleClick = (value: string | null) => { + setOpenedItem(hour) + } + return ( + + + Events {qty} + + {openedItem === hour ? + + + + : <> + } + + + + ); +}; + +export default DayEventsAccordion; \ No newline at end of file diff --git a/src/shared/components/accordion/EventsAccordion.tsx b/src/shared/components/accordion/EventsAccordion.tsx new file mode 100644 index 0000000..0e94e65 --- /dev/null +++ b/src/shared/components/accordion/EventsAccordion.tsx @@ -0,0 +1,143 @@ +import { Accordion, Center, Text } from '@mantine/core'; +import { observer } from 'mobx-react-lite'; +import React, { useContext, useEffect, useState } from 'react'; +import { Context } from '../../..'; +import { useQuery } from '@tanstack/react-query'; +import { frigateQueryKeys, proxyApi } from '../../../services/frigate.proxy/frigate.api'; +import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema'; +import PlayControl from './PlayControl'; +import VideoPlayer from '../frigate/VideoPlayer'; +import { formatUnixTimestampToDateTime, getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../frigate/dateUtil'; + +/** + * @param day frigate format, e.g day: 2024-02-23 + * @param hour frigate format, e.g hour: 22 + * @param cameraName e.g Backyard + * @param hostName proxy format, e.g hostName: localhost:4000 + */ +interface EventsAccordionProps { + day?: string, + hour?: string, + cameraName?: string + hostName?: string +} + +/** + * @param day frigate format, e.g day: 2024-02-23 + * @param hour frigate format, e.g hour: 22 + * @param cameraName e.g Backyard + * @param hostName proxy format, e.g hostName: localhost:4000 + */ +const EventsAccordion = observer(({ + day, + hour, + cameraName, + hostName, + // TODO labels, score +}: EventsAccordionProps) => { + const { recordingsStore: recStore } = useContext(Context) + const [openVideoPlayer, setOpenVideoPlayer] = useState() + const [openedValue, setOpenedValue] = useState() + const [playerUrl, setPlayerUrl] = useState() + + const inHostName = hostName || recStore.recordToPlay.hostName + const inCameraName = cameraName || recStore.recordToPlay.cameraName + const isRequiredParams = inCameraName && inHostName + const { data, isPending, isError, refetch } = useQuery({ + queryKey: [frigateQueryKeys.getEvents, day, hour, inCameraName, inHostName], + queryFn: () => { + if (!isRequiredParams) return null + const [startTime, endTime] = getUnixTime(day, hour) + const parsed = getEventsQuerySchema.safeParse({ + hostName: inHostName, + camerasName: [inCameraName], + after: startTime, + before: endTime, + hasClip: true, + includeThumnails: false, + }) + if (parsed.success) { + return proxyApi.getEvents( + parsed.data.hostName, + parsed.data.camerasName, + parsed.data.timezone, + parsed.data.hasClip, + parsed.data.after, + parsed.data.before, + parsed.data.labels, + parsed.data.limit, + parsed.data.includeThumnails, + parsed.data.minScore, + parsed.data.maxScore + ) + } + return null + } + }) + + useEffect(() => { + if (openVideoPlayer) { + console.log('openVideoPlayer', openVideoPlayer) + if (openVideoPlayer && inHostName) { + const url = proxyApi.eventURL(inHostName, openVideoPlayer) + console.log('GET EVENT URL: ', url) + setPlayerUrl(url) + } + } else { + setPlayerUrl(undefined) + } + }, [openVideoPlayer]) + + if (isPending) return
Loading...
+ if (isError) return
Loading error
+ if (!data || data.length < 1) return
Not have events at that period
+ + const handleOpenPlayer = (eventId: string) => { + // console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`) + if (openVideoPlayer !== eventId) { + setOpenedValue(eventId) + setOpenVideoPlayer(eventId) + } else if (openedValue === eventId && openVideoPlayer === eventId) { + setOpenVideoPlayer(undefined) + } + } + + const handleClick = (value: string) => { + if (openedValue === value) { + setOpenedValue(undefined) + } else { + setOpenedValue(value) + } + setOpenVideoPlayer(undefined) + } + + return ( + + {data.slice(0, 5).map(event => ( + + + + + + {openVideoPlayer === event.id && playerUrl ? : <>} + Camera: {event.camera} + Label: {event.label} + Start: {unixTimeToDate(event.start_time)} + Duration: {getDurationFromTimestamps(event.start_time, event.end_time)} + + + ))} + + ); +}) + +export default EventsAccordion; \ No newline at end of file diff --git a/src/shared/components/accordion/PlayControl.tsx b/src/shared/components/accordion/PlayControl.tsx index d5ea6ae..0208329 100644 --- a/src/shared/components/accordion/PlayControl.tsx +++ b/src/shared/components/accordion/PlayControl.tsx @@ -3,13 +3,15 @@ import { IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; import React from 'react'; interface PlayControlProps { - hour: string, + label: string, + value: string, openVideoPlayer?: string, onClick?: (value: string) => void } const PlayControl = ({ - hour, + label, + value, openVideoPlayer, onClick }: PlayControlProps) => { @@ -18,23 +20,23 @@ const PlayControl = ({ } return ( - Hour: {hour} + {label} { event.stopPropagation() - handleClick(hour) + handleClick(value) }}> - {openVideoPlayer === hour ? 'Stop Video' : 'Play Video'} + {openVideoPlayer === value ? 'Stop Video' : 'Play Video'} - {openVideoPlayer === hour ? + {openVideoPlayer === value ? { event.stopPropagation() - handleClick(hour) + handleClick(value) }} /> : { event.stopPropagation() - handleClick(hour) + handleClick(value) }} /> } diff --git a/src/shared/components/filters.aps/CameraSelectFilter.tsx b/src/shared/components/filters.aps/CameraSelectFilter.tsx new file mode 100644 index 0000000..08c75db --- /dev/null +++ b/src/shared/components/filters.aps/CameraSelectFilter.tsx @@ -0,0 +1,64 @@ +import { observer } from 'mobx-react-lite'; +import React, { useContext, useEffect } from 'react'; +import { Context } from '../../..'; +import { useQuery } from '@tanstack/react-query'; +import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api'; +import CogwheelLoader from '../CogwheelLoader'; +import { Center, Text } from '@mantine/core'; +import OneSelectFilter, { OneSelectItem } from './OneSelectFilter'; + +interface CameraSelectFilterProps { + selectedHostId: string, +} + +const CameraSelectFilter = ({ + selectedHostId, +}: CameraSelectFilterProps) => { + const { recordingsStore: recStore } = useContext(Context) + + const { data, isError, isPending, isSuccess } = useQuery({ + queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId], + queryFn: () => frigateApi.getHost(selectedHostId) + }) + + useEffect(() => { + if (!data) return + if (recStore.cameraIdParam) { + console.log('change camera by param') + recStore.selectedCamera = data.cameras.find( camera => camera.id === recStore.cameraIdParam) + recStore.cameraIdParam = undefined + } + }, [isSuccess]) + + if (isPending) return + if (isError) return
Loading error!
+ if (!data) return null + + const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name })) + + const handleSelect = (id: string, value: string) => { + const camera = data.cameras.find(camera => camera.id === value) + if (!camera) { + recStore.selectedCamera = undefined + return + } + recStore.selectedCamera = camera + } + + console.log('CameraSelectFilter rendered') + // console.log('recStore.selectedCameraId', recStore.selectedCameraId) + + return ( + + ); +}; + +export default observer(CameraSelectFilter); \ No newline at end of file diff --git a/src/shared/components/filters.aps/OneSelectFilter.tsx b/src/shared/components/filters.aps/OneSelectFilter.tsx index 4e9b29b..11a5cb8 100644 --- a/src/shared/components/filters.aps/OneSelectFilter.tsx +++ b/src/shared/components/filters.aps/OneSelectFilter.tsx @@ -21,7 +21,8 @@ interface OneSelectFilterProps { selectProps?: SelectProps, display?: SystemProp showClose?: boolean, - changedState?(id: string, value: string): void + value?: string, + onChange?(id: string, value: string): void onClose?(): void } @@ -29,12 +30,12 @@ interface OneSelectFilterProps { const OneSelectFilter = ({ id, data, spaceBetween, label, defaultValue, textClassName, - selectProps, display, showClose, changedState, onClose + selectProps, display, showClose, value, onChange, onClose }: OneSelectFilterProps) => { const handleOnChange = (value: string) => { - if (changedState) { - changedState(id, value) + if (onChange) { + onChange(id, value) } } @@ -50,6 +51,7 @@ const OneSelectFilter = ({ mt={spaceBetween} data={data} defaultValue={defaultValue} + value={value} onChange={handleOnChange} searchable clearable diff --git a/src/shared/components/frigate/VideoPlayer.tsx b/src/shared/components/frigate/VideoPlayer.tsx index 1c551a6..9c883a8 100644 --- a/src/shared/components/frigate/VideoPlayer.tsx +++ b/src/shared/components/frigate/VideoPlayer.tsx @@ -4,7 +4,7 @@ import Player from 'video.js/dist/types/player'; import 'video.js/dist/video-js.css' interface VideoPlayerProps { - videoUrl?: string + videoUrl: string } const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => { diff --git a/src/shared/components/frigate/dateUtil.ts b/src/shared/components/frigate/dateUtil.ts index d2f5cb4..ee7cd69 100644 --- a/src/shared/components/frigate/dateUtil.ts +++ b/src/shared/components/frigate/dateUtil.ts @@ -17,6 +17,40 @@ export const getNowYesterdayInLong = (): number => { return dateToLong(getNowYesterday()); }; +export const unixTimeToDate = (unixTime: number) => { + const date = new Date(unixTime * 1000); + + const formattedDate = date.getFullYear() + + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + + '-' + ('0' + date.getDate()).slice(-2) + + ' ' + ('0' + date.getHours()).slice(-2) + + ':' + ('0' + date.getMinutes()).slice(-2) + + ':' + ('0' + date.getSeconds()).slice(-2); + + return formattedDate; +} + +/** + * @param day frigate format, e.g day: 2024-02-23 + * @param hour frigate format, e.g hour: 22 + * @returns [start: unixTimeStart, end: unixTimeEnd] + */ +export const getUnixTime = (day?: string, hour?: number | string) => { + if (!day) return [] + let startHour: Date + let endHour: Date + if (!hour || hour === 0) { + startHour = new Date(`${day}T00:00:00`); + endHour = new Date(`${day}T23:59:59`); + } else { + startHour = new Date(`${day}T${hour}:00:00`); + endHour = new Date(`${day}T${hour}:59:59`); + } + const unixTimeStart = startHour.getTime() / 1000; + const unixTimeEnd = endHour.getTime() / 1000; + return [unixTimeStart, unixTimeEnd]; +} + /** * This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string, * and returns a formatted date/time string. @@ -80,7 +114,7 @@ const formatMap: { * The returned string will either be a named time zone (e.g., "America/Los_Angeles"), or it will follow * the format "UTC±HH:MM". */ -const getResolvedTimeZone = () => { +export const getResolvedTimeZone = () => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (error) { @@ -88,8 +122,8 @@ const getResolvedTimeZone = () => { return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60) .toString() .padStart(2, '0')}:${Math.abs(offsetMinutes % 60) - .toString() - .padStart(2, '0')}`; + .toString() + .padStart(2, '0')}`; } }; @@ -176,12 +210,12 @@ interface DurationToken { * @param end_time: number|null - Unix timestamp for end time * @returns string - duration or 'In Progress' if end time is not provided */ -export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => { +export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string => { if (isNaN(start_time)) { return 'Invalid start time'; } let duration = 'In Progress'; - if (end_time !== null) { + if (end_time) { if (isNaN(end_time)) { return 'Invalid end time'; } diff --git a/src/shared/stores/recordings.store.ts b/src/shared/stores/recordings.store.ts index fec35a7..ed5760b 100644 --- a/src/shared/stores/recordings.store.ts +++ b/src/shared/stores/recordings.store.ts @@ -1,16 +1,20 @@ +import { makeAutoObservable } from "mobx" import { z } from "zod" +import { GetCameraWHostWConfig, GetFrigateHost, GetFrigateHostWithCameras } from "../../services/frigate.proxy/frigate.schema" -export type RecordingPlay = { - hostName?: string +export type RecordForPlay = { + hostName?: string // format 'localhost:4000' cameraName?: string hour?: string day?: string timezone?: string } - - export class RecordingsStore { + constructor() { + makeAutoObservable(this) + } + recordingSchema = z.object({ hostName: z.string(), cameraName: z.string(), @@ -19,15 +23,46 @@ export class RecordingsStore { timezone: z.string(), }) - private _playedRecord: RecordingPlay = {} - public get playedRecord(): RecordingPlay { - return this._playedRecord + private _recordToPlay: RecordForPlay = {} + public get recordToPlay(): RecordForPlay { + return this._recordToPlay } - public set playedRecord(value: RecordingPlay) { - this._playedRecord = value + public set recordToPlay(value: RecordForPlay) { + this._recordToPlay = value } - - getFullPlayedRecord(value: RecordingPlay) { + getFullRecordForPlay(value: RecordForPlay) { return this.recordingSchema.safeParse(value) } + + private _hostIdParam: string | undefined + public get hostIdParam(): string | undefined { + return this._hostIdParam + } + public set hostIdParam(value: string | undefined) { + this._hostIdParam = value + } + private _cameraIdParam: string | undefined + public get cameraIdParam(): string | undefined { + return this._cameraIdParam + } + public set cameraIdParam(value: string | undefined) { + this._cameraIdParam = value + } + + private _selectedHost: GetFrigateHost | undefined + public get selectedHost(): GetFrigateHost | undefined { + return this._selectedHost + } + public set selectedHost(value: GetFrigateHost | undefined) { + this._selectedHost = value + } + private _selectedCamera: GetCameraWHostWConfig | undefined + public get selectedCamera(): GetCameraWHostWConfig | undefined { + return this._selectedCamera + } + public set selectedCamera(value: GetCameraWHostWConfig | undefined) { + this._selectedCamera = value + } + selectedStartDay: string = '' + selectedEndDay: string = '' } \ No newline at end of file diff --git a/src/types/event.ts b/src/types/event.ts index 0e7aa99..83a5065 100644 --- a/src/types/event.ts +++ b/src/types/event.ts @@ -1,4 +1,4 @@ -export interface Event { +export interface EventFrigate { id: string; label: string; sub_label?: string; diff --git a/src/widgets/RecordingsFiltersRightSide.tsx b/src/widgets/RecordingsFiltersRightSide.tsx new file mode 100644 index 0000000..b0a8199 --- /dev/null +++ b/src/widgets/RecordingsFiltersRightSide.tsx @@ -0,0 +1,76 @@ +import React, { useContext, useEffect } from 'react'; +import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter'; +import { observer } from 'mobx-react-lite'; +import { Context } from '..'; +import { useQuery } from '@tanstack/react-query'; +import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; +import { Center, Text } from '@mantine/core'; +import CogwheelLoader from '../shared/components/CogwheelLoader'; +import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter'; + +interface RecordingsFiltersRightSideProps { +} + +const RecordingsFiltersRightSide = ({ +}: RecordingsFiltersRightSideProps) => { + const { recordingsStore: recStore } = useContext(Context) + + const { data: hosts, isError, isPending, isSuccess } = useQuery({ + queryKey: [frigateQueryKeys.getFrigateHosts], + queryFn: frigateApi.getHosts + }) + + useEffect(() => { + if (!hosts) return + if (recStore.hostIdParam) { + recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam) + recStore.hostIdParam = undefined + } + }, [isSuccess]) + + if (isPending) return + if (isError) return
Loading error!
+ + if (!hosts || hosts.length < 1) return null + + const hostItems: OneSelectItem[] = hosts + .filter(host => host.enabled) + .map(host => ({ value: host.id, label: host.name })) + + const handleSelect = (id: string, value: string) => { + const host = hosts?.find(host => host.id === value) + if (!host) { + recStore.selectedHost = undefined + recStore.selectedCamera = undefined + return + } + if (recStore.selectedHost?.id !== host.id) { + recStore.selectedCamera = undefined + } + recStore.selectedHost = host + } + + console.log('RecordingsFiltersRightSide rendered') + return ( + <> + + {recStore.selectedHost ? + + : + <> + } + + + ) +} + +export default observer(RecordingsFiltersRightSide); \ No newline at end of file diff --git a/src/widgets/SelectedCameraList.tsx b/src/widgets/SelectedCameraList.tsx new file mode 100644 index 0000000..fd2beec --- /dev/null +++ b/src/widgets/SelectedCameraList.tsx @@ -0,0 +1,51 @@ +import { Center, Flex, Text } from '@mantine/core'; +import React, { Suspense, useContext } from 'react'; +import CameraAccordion from '../shared/components/accordion/CameraAccordion'; +import { GetCameraWHostWConfig, GetFrigateHost } from '../services/frigate.proxy/frigate.schema'; +import { useQuery } from '@tanstack/react-query'; +import { Context } from '..'; +import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api'; +import { host } from '../shared/env.const'; +import CogwheelLoader from '../shared/components/CogwheelLoader'; +import RetryError from '../pages/RetryError'; + +interface SelectedCameraListProps { + cameraId: string, +} + +const SelectedCameraList = ({ + cameraId, +}: SelectedCameraListProps) => { + + const { recordingsStore: recStore } = useContext(Context) + + const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({ + queryKey: [frigateQueryKeys.getCameraWHost, cameraId], + queryFn: async () => { + if (cameraId) { + return frigateApi.getCameraWHost(cameraId) + } + return null + } + }) + + const handleRetry = () => { + cameraRefetch() + } + + if (cameraPending) return + if (cameraError) return + + if (!camera?.frigateHost) return null + + return ( + + {camera.frigateHost.name} / {camera.name} + + + + + ) +}; + +export default SelectedCameraList; \ No newline at end of file diff --git a/src/widgets/SelectedHostList.tsx b/src/widgets/SelectedHostList.tsx new file mode 100644 index 0000000..c8021e5 --- /dev/null +++ b/src/widgets/SelectedHostList.tsx @@ -0,0 +1,75 @@ +import { Accordion, Flex, Text } from '@mantine/core'; +import React, { Suspense, lazy, useContext, useState } from 'react'; +import { host } from '../shared/env.const'; +import { useQuery } from '@tanstack/react-query'; +import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api'; +import { Context } from '..'; +import CenterLoader from '../shared/components/CenterLoader'; +import RetryError from '../pages/RetryError'; +const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion')); + + +interface SelectedHostListProps { + hostId: string +} + +const SelectedHostList = ({ + hostId +}: SelectedHostListProps) => { + + const { recordingsStore: recStore } = useContext(Context) + const [openCameraId, setOpenCameraId] = useState(null) + + const { data: host, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({ + queryKey: [frigateQueryKeys.getFrigateHost, hostId], + queryFn: async () => { + if (hostId) { + return frigateApi.getHost(hostId) + } + return null + } + }) + + const handleOnChange = (cameraId: string | null) => { + setOpenCameraId(openCameraId === cameraId ? null : cameraId) + } + + const handleRetry = () => { + if (recStore.selectedHost) hostRefetch() + } + + if (hostPending) return + if (hostError) return + + if (!host || host.cameras.length < 1) return null + + const cameras = host.cameras.slice(0, 2).map(camera => { + return ( + + {camera.name} + + {openCameraId === camera.id && ( + + + + )} + + + ) + }) + + return ( + + {host.name} + handleOnChange(value)}> + {cameras} + + + ) +}; + +export default SelectedHostList; \ No newline at end of file