mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Fix linter and fix lint issues (#10141)
This commit is contained in:
		
							parent
							
								
									b6ef1e4330
								
							
						
					
					
						commit
						3bf2a496e1
					
				| @ -1,12 +1,13 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
|   root: true, |   root: true, | ||||||
|   env: { browser: true, es2021: true, "vitest-globals/env": true }, |  | ||||||
|   extends: [ |   extends: [ | ||||||
|     "eslint:recommended", |     "eslint:recommended", | ||||||
|     "plugin:@typescript-eslint/recommended", |     "plugin:@typescript-eslint/recommended", | ||||||
|     "plugin:react-hooks/recommended", |     "plugin:react-hooks/recommended", | ||||||
|     "plugin:prettier", |     "plugin:vitest-globals/recommended", | ||||||
|  |     "plugin:prettier/recommended", | ||||||
|   ], |   ], | ||||||
|  |   env: { browser: true, es2021: true, "vitest-globals/env": true }, | ||||||
|   ignorePatterns: ["dist", ".eslintrc.cjs"], |   ignorePatterns: ["dist", ".eslintrc.cjs"], | ||||||
|   parser: "@typescript-eslint/parser", |   parser: "@typescript-eslint/parser", | ||||||
|   parserOptions: { |   parserOptions: { | ||||||
| @ -21,9 +22,11 @@ module.exports = { | |||||||
|       version: 27, |       version: 27, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   ignorePatterns: ["*.d.ts"], |   ignorePatterns: ["*.d.ts", "/src/components/ui/*"], | ||||||
|   plugins: ["react-refresh"], |   plugins: ["react-hooks", "react-refresh"], | ||||||
|   rules: { |   rules: { | ||||||
|  |     "react-hooks/rules-of-hooks": "error", | ||||||
|  |     "react-hooks/exhaustive-deps": "error", | ||||||
|     "react-refresh/only-export-components": [ |     "react-refresh/only-export-components": [ | ||||||
|       "warn", |       "warn", | ||||||
|       { allowConstantExport: true }, |       { allowConstantExport: true }, | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -78,7 +78,7 @@ | |||||||
|         "@vitejs/plugin-react-swc": "^3.6.0", |         "@vitejs/plugin-react-swc": "^3.6.0", | ||||||
|         "@vitest/coverage-v8": "^1.0.0", |         "@vitest/coverage-v8": "^1.0.0", | ||||||
|         "autoprefixer": "^10.4.16", |         "autoprefixer": "^10.4.16", | ||||||
|         "eslint": "^8.53.0", |         "eslint": "^8.55.0", | ||||||
|         "eslint-config-prettier": "^9.1.0", |         "eslint-config-prettier": "^9.1.0", | ||||||
|         "eslint-plugin-jest": "^27.6.0", |         "eslint-plugin-jest": "^27.6.0", | ||||||
|         "eslint-plugin-prettier": "^5.0.1", |         "eslint-plugin-prettier": "^5.0.1", | ||||||
|  | |||||||
| @ -83,7 +83,7 @@ | |||||||
|     "@vitejs/plugin-react-swc": "^3.6.0", |     "@vitejs/plugin-react-swc": "^3.6.0", | ||||||
|     "@vitest/coverage-v8": "^1.0.0", |     "@vitest/coverage-v8": "^1.0.0", | ||||||
|     "autoprefixer": "^10.4.16", |     "autoprefixer": "^10.4.16", | ||||||
|     "eslint": "^8.53.0", |     "eslint": "^8.55.0", | ||||||
|     "eslint-config-prettier": "^9.1.0", |     "eslint-config-prettier": "^9.1.0", | ||||||
|     "eslint-plugin-jest": "^27.6.0", |     "eslint-plugin-jest": "^27.6.0", | ||||||
|     "eslint-plugin-prettier": "^5.0.1", |     "eslint-plugin-prettier": "^5.0.1", | ||||||
|  | |||||||
| @ -3,4 +3,4 @@ export default { | |||||||
|     tailwindcss: {}, |     tailwindcss: {}, | ||||||
|     autoprefixer: {}, |     autoprefixer: {}, | ||||||
|   }, |   }, | ||||||
| } | }; | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| declare global { | declare global { | ||||||
|     interface Window { |   interface Window { | ||||||
|       baseUrl?: any; |     baseUrl?: string; | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`; | export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || "/"}`; | ||||||
|  | |||||||
| @ -9,12 +9,12 @@ import { createContainer } from "react-tracked"; | |||||||
| 
 | 
 | ||||||
| type Update = { | type Update = { | ||||||
|   topic: string; |   topic: string; | ||||||
|   payload: any; |   payload: unknown; | ||||||
|   retain: boolean; |   retain: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type WsState = { | type WsState = { | ||||||
|   [topic: string]: any; |   [topic: string]: unknown; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type useValueReturn = [WsState, (update: Update) => void]; | type useValueReturn = [WsState, (update: Update) => void]; | ||||||
| @ -47,6 +47,8 @@ function useValue(): useValueReturn { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     setWsState({ ...wsState, ...cameraStates }); |     setWsState({ ...wsState, ...cameraStates }); | ||||||
|  |     // we only want this to run initially when the config is loaded
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [config]); |   }, [config]); | ||||||
| 
 | 
 | ||||||
|   // ws handler
 |   // ws handler
 | ||||||
| @ -72,7 +74,7 @@ function useValue(): useValueReturn { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [readyState, sendJsonMessage] |     [readyState, sendJsonMessage], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return [wsState, setState]; |   return [wsState, setState]; | ||||||
| @ -91,14 +93,14 @@ export function useWs(watchTopic: string, publishTopic: string) { | |||||||
|   const value = { payload: state[watchTopic] || null }; |   const value = { payload: state[watchTopic] || null }; | ||||||
| 
 | 
 | ||||||
|   const send = useCallback( |   const send = useCallback( | ||||||
|     (payload: any, retain = false) => { |     (payload: unknown, retain = false) => { | ||||||
|       sendJsonMessage({ |       sendJsonMessage({ | ||||||
|         topic: publishTopic || watchTopic, |         topic: publishTopic || watchTopic, | ||||||
|         payload, |         payload, | ||||||
|         retain, |         retain, | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [sendJsonMessage, watchTopic, publishTopic] |     [sendJsonMessage, watchTopic, publishTopic], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return { value, send }; |   return { value, send }; | ||||||
| @ -112,7 +114,7 @@ export function useDetectState(camera: string): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs(`${camera}/detect/state`, `${camera}/detect/set`); |   } = useWs(`${camera}/detect/state`, `${camera}/detect/set`); | ||||||
|   return { payload, send }; |   return { payload: payload as ToggleableSetting, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useRecordingsState(camera: string): { | export function useRecordingsState(camera: string): { | ||||||
| @ -123,7 +125,7 @@ export function useRecordingsState(camera: string): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`); |   } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`); | ||||||
|   return { payload, send }; |   return { payload: payload as ToggleableSetting, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useSnapshotsState(camera: string): { | export function useSnapshotsState(camera: string): { | ||||||
| @ -134,7 +136,7 @@ export function useSnapshotsState(camera: string): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`); |   } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`); | ||||||
|   return { payload, send }; |   return { payload: payload as ToggleableSetting, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useAudioState(camera: string): { | export function useAudioState(camera: string): { | ||||||
| @ -145,7 +147,7 @@ export function useAudioState(camera: string): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); |   } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); | ||||||
|   return { payload, send }; |   return { payload: payload as ToggleableSetting, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function usePtzCommand(camera: string): { | export function usePtzCommand(camera: string): { | ||||||
| @ -156,7 +158,7 @@ export function usePtzCommand(camera: string): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs(`${camera}/ptz`, `${camera}/ptz`); |   } = useWs(`${camera}/ptz`, `${camera}/ptz`); | ||||||
|   return { payload, send }; |   return { payload: payload as string, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useRestart(): { | export function useRestart(): { | ||||||
| @ -167,40 +169,40 @@ export function useRestart(): { | |||||||
|     value: { payload }, |     value: { payload }, | ||||||
|     send, |     send, | ||||||
|   } = useWs("restart", "restart"); |   } = useWs("restart", "restart"); | ||||||
|   return { payload, send }; |   return { payload: payload as string, send }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useFrigateEvents(): { payload: FrigateEvent } { | export function useFrigateEvents(): { payload: FrigateEvent } { | ||||||
|   const { |   const { | ||||||
|     value: { payload }, |     value: { payload }, | ||||||
|   } = useWs("events", ""); |   } = useWs("events", ""); | ||||||
|   return { payload: JSON.parse(payload) }; |   return { payload: JSON.parse(payload as string) }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useFrigateReviews(): { payload: FrigateReview } { | export function useFrigateReviews(): { payload: FrigateReview } { | ||||||
|   const { |   const { | ||||||
|     value: { payload }, |     value: { payload }, | ||||||
|   } = useWs("reviews", ""); |   } = useWs("reviews", ""); | ||||||
|   return { payload: JSON.parse(payload) }; |   return { payload: JSON.parse(payload as string) }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useFrigateStats(): { payload: FrigateStats } { | export function useFrigateStats(): { payload: FrigateStats } { | ||||||
|   const { |   const { | ||||||
|     value: { payload }, |     value: { payload }, | ||||||
|   } = useWs("stats", ""); |   } = useWs("stats", ""); | ||||||
|   return { payload: JSON.parse(payload) }; |   return { payload: JSON.parse(payload as string) }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useMotionActivity(camera: string): { payload: string } { | export function useMotionActivity(camera: string): { payload: string } { | ||||||
|   const { |   const { | ||||||
|     value: { payload }, |     value: { payload }, | ||||||
|   } = useWs(`${camera}/motion`, ""); |   } = useWs(`${camera}/motion`, ""); | ||||||
|   return { payload }; |   return { payload: payload as string }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useAudioActivity(camera: string): { payload: number } { | export function useAudioActivity(camera: string): { payload: number } { | ||||||
|   const { |   const { | ||||||
|     value: { payload }, |     value: { payload }, | ||||||
|   } = useWs(`${camera}/audio/rms`, ""); |   } = useWs(`${camera}/audio/rms`, ""); | ||||||
|   return { payload }; |   return { payload: payload as number }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { useMemo } from "react"; | |||||||
| import { MdCircle } from "react-icons/md"; | import { MdCircle } from "react-icons/md"; | ||||||
| import useSWR from "swr"; | import useSWR from "swr"; | ||||||
| 
 | 
 | ||||||
| export default function Statusbar({}) { | export default function Statusbar() { | ||||||
|   const { data: initialStats } = useSWR<FrigateStats>("stats", { |   const { data: initialStats } = useSWR<FrigateStats>("stats", { | ||||||
|     revalidateOnFocus: false, |     revalidateOnFocus: false, | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import CameraImage from "./CameraImage"; | |||||||
| 
 | 
 | ||||||
| type AutoUpdatingCameraImageProps = { | type AutoUpdatingCameraImageProps = { | ||||||
|   camera: string; |   camera: string; | ||||||
|   searchParams?: {}; |   searchParams?: URLSearchParams; | ||||||
|   showFps?: boolean; |   showFps?: boolean; | ||||||
|   className?: string; |   className?: string; | ||||||
|   reloadInterval?: number; |   reloadInterval?: number; | ||||||
| @ -13,7 +13,7 @@ const MIN_LOAD_TIMEOUT_MS = 200; | |||||||
| 
 | 
 | ||||||
| export default function AutoUpdatingCameraImage({ | export default function AutoUpdatingCameraImage({ | ||||||
|   camera, |   camera, | ||||||
|   searchParams = "", |   searchParams = undefined, | ||||||
|   showFps = true, |   showFps = true, | ||||||
|   className, |   className, | ||||||
|   reloadInterval = MIN_LOAD_TIMEOUT_MS, |   reloadInterval = MIN_LOAD_TIMEOUT_MS, | ||||||
| @ -35,6 +35,8 @@ export default function AutoUpdatingCameraImage({ | |||||||
|         setTimeoutId(undefined); |         setTimeoutId(undefined); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [reloadInterval]); |   }, [reloadInterval]); | ||||||
| 
 | 
 | ||||||
|   const handleLoad = useCallback(() => { |   const handleLoad = useCallback(() => { | ||||||
| @ -53,9 +55,11 @@ export default function AutoUpdatingCameraImage({ | |||||||
|         () => { |         () => { | ||||||
|           setKey(Date.now()); |           setKey(Date.now()); | ||||||
|         }, |         }, | ||||||
|         loadTime > reloadInterval ? 1 : reloadInterval |         loadTime > reloadInterval ? 1 : reloadInterval, | ||||||
|       ) |       ), | ||||||
|     ); |     ); | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [key, setFps]); |   }, [key, setFps]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ type CameraImageProps = { | |||||||
|   className?: string; |   className?: string; | ||||||
|   camera: string; |   camera: string; | ||||||
|   onload?: () => void; |   onload?: () => void; | ||||||
|   searchParams?: {}; |   searchParams?: string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default function CameraImage({ | export default function CameraImage({ | ||||||
|  | |||||||
| @ -24,25 +24,25 @@ export default function DebugCameraImage({ | |||||||
|   const [showSettings, setShowSettings] = useState(false); |   const [showSettings, setShowSettings] = useState(false); | ||||||
|   const [options, setOptions] = usePersistence<Options>( |   const [options, setOptions] = usePersistence<Options>( | ||||||
|     `${cameraConfig?.name}-feed`, |     `${cameraConfig?.name}-feed`, | ||||||
|     emptyObject |     emptyObject, | ||||||
|   ); |   ); | ||||||
|   const handleSetOption = useCallback( |   const handleSetOption = useCallback( | ||||||
|     (id: string, value: boolean) => { |     (id: string, value: boolean) => { | ||||||
|       const newOptions = { ...options, [id]: value }; |       const newOptions = { ...options, [id]: value }; | ||||||
|       setOptions(newOptions); |       setOptions(newOptions); | ||||||
|     }, |     }, | ||||||
|     [options] |     [options, setOptions], | ||||||
|   ); |   ); | ||||||
|   const searchParams = useMemo( |   const searchParams = useMemo( | ||||||
|     () => |     () => | ||||||
|       new URLSearchParams( |       new URLSearchParams( | ||||||
|         Object.keys(options || {}).reduce((memo, key) => { |         Object.keys(options || {}).reduce((memo, key) => { | ||||||
|           //@ts-ignore we know this is correct
 |           //@ts-expect-error we know this is correct
 | ||||||
|           memo.push([key, options[key] === true ? "1" : "0"]); |           memo.push([key, options[key] === true ? "1" : "0"]); | ||||||
|           return memo; |           return memo; | ||||||
|         }, []) |         }, []), | ||||||
|       ), |       ), | ||||||
|     [options] |     [options], | ||||||
|   ); |   ); | ||||||
|   const handleToggleSettings = useCallback(() => { |   const handleToggleSettings = useCallback(() => { | ||||||
|     setShowSettings(!showSettings); |     setShowSettings(!showSettings); | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ type CameraImageProps = { | |||||||
|   className?: string; |   className?: string; | ||||||
|   camera: string; |   camera: string; | ||||||
|   onload?: (event: Event) => void; |   onload?: (event: Event) => void; | ||||||
|   searchParams?: {}; |   searchParams?: string; | ||||||
|   stretch?: boolean; // stretch to fit width
 |   stretch?: boolean; // stretch to fit width
 | ||||||
|   fitAspect?: number; // shrink to fit height
 |   fitAspect?: number; // shrink to fit height
 | ||||||
| }; | }; | ||||||
| @ -58,10 +58,17 @@ export default function CameraImage({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return 100; |     return 100; | ||||||
|   }, [availableWidth, aspectRatio, height, stretch]); |   }, [ | ||||||
|  |     availableWidth, | ||||||
|  |     aspectRatio, | ||||||
|  |     containerHeight, | ||||||
|  |     fitAspect, | ||||||
|  |     height, | ||||||
|  |     stretch, | ||||||
|  |   ]); | ||||||
|   const scaledWidth = useMemo( |   const scaledWidth = useMemo( | ||||||
|     () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), |     () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), | ||||||
|     [scaledHeight, aspectRatio, scrollBarWidth] |     [scaledHeight, aspectRatio, scrollBarWidth], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const img = useMemo(() => new Image(), []); |   const img = useMemo(() => new Image(), []); | ||||||
| @ -74,7 +81,7 @@ export default function CameraImage({ | |||||||
|       } |       } | ||||||
|       onload && onload(event); |       onload && onload(event); | ||||||
|     }, |     }, | ||||||
|     [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef] |     [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { useFrigateReviews } from "@/api/ws"; | |||||||
| import { ReviewSeverity } from "@/types/review"; | import { ReviewSeverity } from "@/types/review"; | ||||||
| import { Button } from "../ui/button"; | import { Button } from "../ui/button"; | ||||||
| import { LuRefreshCcw } from "react-icons/lu"; | import { LuRefreshCcw } from "react-icons/lu"; | ||||||
| import { MutableRefObject, useEffect, useState } from "react"; | import { MutableRefObject, useEffect, useMemo, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| type NewReviewDataProps = { | type NewReviewDataProps = { | ||||||
|   className: string; |   className: string; | ||||||
| @ -18,7 +18,8 @@ export default function NewReviewData({ | |||||||
| }: NewReviewDataProps) { | }: NewReviewDataProps) { | ||||||
|   const { payload: review } = useFrigateReviews(); |   const { payload: review } = useFrigateReviews(); | ||||||
| 
 | 
 | ||||||
|   const [reviewId, setReviewId] = useState(""); |   const startCheckTs = useMemo(() => Date.now() / 1000, []); | ||||||
|  |   const [reviewTs, setReviewTs] = useState(startCheckTs); | ||||||
|   const [hasUpdate, setHasUpdate] = useState(false); |   const [hasUpdate, setHasUpdate] = useState(false); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -27,15 +28,15 @@ export default function NewReviewData({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (review.type == "end" && review.review.severity == severity) { |     if (review.type == "end" && review.review.severity == severity) { | ||||||
|       setReviewId(review.review.id); |       setReviewTs(review.review.start_time); | ||||||
|     } |     } | ||||||
|   }, [review]); |   }, [review, severity]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (reviewId != "") { |     if (reviewTs > startCheckTs) { | ||||||
|       setHasUpdate(true); |       setHasUpdate(true); | ||||||
|     } |     } | ||||||
|   }, [reviewId]); |   }, [startCheckTs, reviewTs]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={className}> |     <div className={className}> | ||||||
|  | |||||||
| @ -91,7 +91,7 @@ const TimeAgo: FunctionComponent<IProp> = ({ | |||||||
|     } else { |     } else { | ||||||
|       return 3600000; // refresh every hour
 |       return 3600000; // refresh every hour
 | ||||||
|     } |     } | ||||||
|   }, [currentTime, manualRefreshInterval]); |   }, [currentTime, manualRefreshInterval, time]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const intervalId: NodeJS.Timeout = setInterval(() => { |     const intervalId: NodeJS.Timeout = setInterval(() => { | ||||||
| @ -102,7 +102,7 @@ const TimeAgo: FunctionComponent<IProp> = ({ | |||||||
| 
 | 
 | ||||||
|   const timeAgoValue = useMemo( |   const timeAgoValue = useMemo( | ||||||
|     () => timeAgo({ time, currentTime, ...rest }), |     () => timeAgo({ time, currentTime, ...rest }), | ||||||
|     [currentTime, rest] |     [currentTime, rest, time], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return <span>{timeAgoValue}</span>; |   return <span>{timeAgoValue}</span>; | ||||||
|  | |||||||
| @ -54,7 +54,7 @@ export default function ReviewFilterGroup({ | |||||||
|       cameras: Object.keys(config?.cameras || {}), |       cameras: Object.keys(config?.cameras || {}), | ||||||
|       labels: Object.values(allLabels || {}), |       labels: Object.values(allLabels || {}), | ||||||
|     }), |     }), | ||||||
|     [config, allLabels] |     [config, allLabels], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // handle updating filters
 |   // handle updating filters
 | ||||||
| @ -67,7 +67,7 @@ export default function ReviewFilterGroup({ | |||||||
|         before: day == undefined ? undefined : getEndOfDayTimestamp(day), |         before: day == undefined ? undefined : getEndOfDayTimestamp(day), | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [onUpdateFilter] |     [filter, onUpdateFilter], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -111,7 +111,7 @@ function CamerasFilterButton({ | |||||||
|   updateCameraFilter, |   updateCameraFilter, | ||||||
| }: CameraFilterButtonProps) { | }: CameraFilterButtonProps) { | ||||||
|   const [currentCameras, setCurrentCameras] = useState<string[] | undefined>( |   const [currentCameras, setCurrentCameras] = useState<string[] | undefined>( | ||||||
|     selectedCameras |     selectedCameras, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -200,7 +200,7 @@ function CalendarFilterButton({ | |||||||
|   }, []); |   }, []); | ||||||
|   const selectedDate = useFormattedTimestamp( |   const selectedDate = useFormattedTimestamp( | ||||||
|     day == undefined ? 0 : day?.getTime() / 1000, |     day == undefined ? 0 : day?.getTime() / 1000, | ||||||
|     "%b %-d" |     "%b %-d", | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -273,7 +273,7 @@ function GeneralFilterButton({ | |||||||
|           <Button |           <Button | ||||||
|             className="capitalize flex justify-between items-center cursor-pointer w-full" |             className="capitalize flex justify-between items-center cursor-pointer w-full" | ||||||
|             variant="secondary" |             variant="secondary" | ||||||
|             onClick={(_) => setShowReviewed(showReviewed == 0 ? 1 : 0)} |             onClick={() => setShowReviewed(showReviewed == 0 ? 1 : 0)} | ||||||
|           > |           > | ||||||
|             {showReviewed ? ( |             {showReviewed ? ( | ||||||
|               <LuCheck className="w-6 h-6" /> |               <LuCheck className="w-6 h-6" /> | ||||||
| @ -299,7 +299,7 @@ function LabelsFilterButton({ | |||||||
|   updateLabelFilter, |   updateLabelFilter, | ||||||
| }: LabelFilterButtonProps) { | }: LabelFilterButtonProps) { | ||||||
|   const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( |   const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( | ||||||
|     selectedLabels |     selectedLabels, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -375,7 +375,7 @@ function FilterCheckBox({ | |||||||
|     <Button |     <Button | ||||||
|       className="capitalize flex justify-between items-center cursor-pointer w-full" |       className="capitalize flex justify-between items-center cursor-pointer w-full" | ||||||
|       variant="ghost" |       variant="ghost" | ||||||
|       onClick={(_) => onCheckedChange(!isChecked)} |       onClick={() => onCheckedChange(!isChecked)} | ||||||
|     > |     > | ||||||
|       {isChecked ? ( |       {isChecked ? ( | ||||||
|         <LuCheck className="w-6 h-6" /> |         <LuCheck className="w-6 h-6" /> | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return `${baseUrl}api/review/${event.id}/preview.gif`; |     return `${baseUrl}api/review/${event.id}/preview.gif`; | ||||||
|   }, [event]); |   }, [apiHost, event]); | ||||||
| 
 | 
 | ||||||
|   const aspectRatio = useMemo(() => { |   const aspectRatio = useMemo(() => { | ||||||
|     if (!config) { |     if (!config) { | ||||||
| @ -39,7 +39,7 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) { | |||||||
| 
 | 
 | ||||||
|     const detect = config.cameras[event.camera].detect; |     const detect = config.cameras[event.camera].detect; | ||||||
|     return detect.width / detect.height; |     return detect.width / detect.height; | ||||||
|   }, [config]); |   }, [config, event]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Tooltip> |     <Tooltip> | ||||||
|  | |||||||
| @ -4,20 +4,20 @@ import SettingsNavItems from "../settings/SettingsNavItems"; | |||||||
| 
 | 
 | ||||||
| function Bottombar() { | function Bottombar() { | ||||||
|   return ( |   return ( | ||||||
|       <div className="absolute h-16 inset-x-4 bottom-0 flex flex-row items-center justify-between"> |     <div className="absolute h-16 inset-x-4 bottom-0 flex flex-row items-center justify-between"> | ||||||
|         {navbarLinks.map((item) => ( |       {navbarLinks.map((item) => ( | ||||||
|           <NavItem |         <NavItem | ||||||
|             className="" |           className="" | ||||||
|             variant="secondary" |           variant="secondary" | ||||||
|             key={item.id} |           key={item.id} | ||||||
|             Icon={item.icon} |           Icon={item.icon} | ||||||
|             title={item.title} |           title={item.title} | ||||||
|             url={item.url} |           url={item.url} | ||||||
|             dev={item.dev} |           dev={item.dev} | ||||||
|           /> |         /> | ||||||
|         ))} |       ))} | ||||||
|       <SettingsNavItems className="flex flex-shrink-0 justify-between gap-4" /> |       <SettingsNavItems className="flex flex-shrink-0 justify-between gap-4" /> | ||||||
|       </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,19 +14,6 @@ export default function TimelineEventOverlay({ | |||||||
|   timeline, |   timeline, | ||||||
|   cameraConfig, |   cameraConfig, | ||||||
| }: TimelineEventOverlayProps) { | }: TimelineEventOverlayProps) { | ||||||
|   if (!timeline.data.box) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const boxLeftEdge = Math.round(timeline.data.box[0] * 100); |  | ||||||
|   const boxTopEdge = Math.round(timeline.data.box[1] * 100); |  | ||||||
|   const boxRightEdge = Math.round( |  | ||||||
|     (1 - timeline.data.box[2] - timeline.data.box[0]) * 100 |  | ||||||
|   ); |  | ||||||
|   const boxBottomEdge = Math.round( |  | ||||||
|     (1 - timeline.data.box[3] - timeline.data.box[1]) * 100 |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const [isHovering, setIsHovering] = useState<boolean>(false); |   const [isHovering, setIsHovering] = useState<boolean>(false); | ||||||
|   const getHoverStyle = () => { |   const getHoverStyle = () => { | ||||||
|     if (!timeline.data.box) { |     if (!timeline.data.box) { | ||||||
| @ -67,6 +54,19 @@ export default function TimelineEventOverlay({ | |||||||
|     return Math.round(100 * (width / height)) / 100; |     return Math.round(100 * (width / height)) / 100; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   if (!timeline.data.box) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const boxLeftEdge = Math.round(timeline.data.box[0] * 100); | ||||||
|  |   const boxTopEdge = Math.round(timeline.data.box[1] * 100); | ||||||
|  |   const boxRightEdge = Math.round( | ||||||
|  |     (1 - timeline.data.box[2] - timeline.data.box[0]) * 100, | ||||||
|  |   ); | ||||||
|  |   const boxBottomEdge = Math.round( | ||||||
|  |     (1 - timeline.data.box[3] - timeline.data.box[1]) * 100, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <div |       <div | ||||||
|  | |||||||
| @ -14,6 +14,9 @@ import useSWR from "swr"; | |||||||
| import { FrigateConfig } from "@/types/frigateConfig"; | import { FrigateConfig } from "@/types/frigateConfig"; | ||||||
| import ActivityIndicator from "../ui/activity-indicator"; | import ActivityIndicator from "../ui/activity-indicator"; | ||||||
| import useKeyboardListener from "@/hooks/use-keyboard-listener"; | import useKeyboardListener from "@/hooks/use-keyboard-listener"; | ||||||
|  | import { Recording } from "@/types/record"; | ||||||
|  | import { Preview } from "@/types/preview"; | ||||||
|  | import { DynamicPlayback } from "@/types/playback"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Dynamically switches between video playback and scrubbing preview player. |  * Dynamically switches between video playback and scrubbing preview player. | ||||||
| @ -37,7 +40,7 @@ export default function DynamicVideoPlayer({ | |||||||
|   const timezone = useMemo( |   const timezone = useMemo( | ||||||
|     () => |     () => | ||||||
|       config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, |       config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, | ||||||
|     [config] |     [config], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // playback behavior
 |   // playback behavior
 | ||||||
| @ -51,7 +54,7 @@ export default function DynamicVideoPlayer({ | |||||||
|         config.cameras[camera].detect.height < |         config.cameras[camera].detect.height < | ||||||
|       1.7 |       1.7 | ||||||
|     ); |     ); | ||||||
|   }, [config]); |   }, [camera, config]); | ||||||
| 
 | 
 | ||||||
|   // controlling playback
 |   // controlling playback
 | ||||||
| 
 | 
 | ||||||
| @ -60,7 +63,7 @@ export default function DynamicVideoPlayer({ | |||||||
|   const [isScrubbing, setIsScrubbing] = useState(false); |   const [isScrubbing, setIsScrubbing] = useState(false); | ||||||
|   const [hasPreview, setHasPreview] = useState(false); |   const [hasPreview, setHasPreview] = useState(false); | ||||||
|   const [focusedItem, setFocusedItem] = useState<Timeline | undefined>( |   const [focusedItem, setFocusedItem] = useState<Timeline | undefined>( | ||||||
|     undefined |     undefined, | ||||||
|   ); |   ); | ||||||
|   const controller = useMemo(() => { |   const controller = useMemo(() => { | ||||||
|     if (!config) { |     if (!config) { | ||||||
| @ -72,9 +75,9 @@ export default function DynamicVideoPlayer({ | |||||||
|       previewRef, |       previewRef, | ||||||
|       (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, |       (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, | ||||||
|       setIsScrubbing, |       setIsScrubbing, | ||||||
|       setFocusedItem |       setFocusedItem, | ||||||
|     ); |     ); | ||||||
|   }, [config]); |   }, [camera, config]); | ||||||
| 
 | 
 | ||||||
|   // keyboard control
 |   // keyboard control
 | ||||||
| 
 | 
 | ||||||
| @ -115,11 +118,11 @@ export default function DynamicVideoPlayer({ | |||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [playerRef] |     [playerRef], | ||||||
|   ); |   ); | ||||||
|   useKeyboardListener( |   useKeyboardListener( | ||||||
|     ["ArrowLeft", "ArrowRight", "m", " "], |     ["ArrowLeft", "ArrowRight", "m", " "], | ||||||
|     onKeyboardShortcut |     onKeyboardShortcut, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // initial state
 |   // initial state
 | ||||||
| @ -131,16 +134,18 @@ export default function DynamicVideoPlayer({ | |||||||
|         date.getMonth() + 1 |         date.getMonth() + 1 | ||||||
|       }/${date.getDate()}/${date.getHours()}/${camera}/${timezone.replaceAll( |       }/${date.getDate()}/${date.getHours()}/${camera}/${timezone.replaceAll( | ||||||
|         "/", |         "/", | ||||||
|         "," |         ",", | ||||||
|       )}/master.m3u8`,
 |       )}/master.m3u8`,
 | ||||||
|       type: "application/vnd.apple.mpegurl", |       type: "application/vnd.apple.mpegurl", | ||||||
|     }; |     }; | ||||||
|  |     // we only want to calculate this once
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
|   const initialPreviewSource = useMemo(() => { |   const initialPreviewSource = useMemo(() => { | ||||||
|     const preview = cameraPreviews.find( |     const preview = cameraPreviews.find( | ||||||
|       (preview) => |       (preview) => | ||||||
|         Math.round(preview.start) >= timeRange.start && |         Math.round(preview.start) >= timeRange.start && | ||||||
|         Math.floor(preview.end) <= timeRange.end |         Math.floor(preview.end) <= timeRange.end, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     if (preview) { |     if (preview) { | ||||||
| @ -153,6 +158,9 @@ export default function DynamicVideoPlayer({ | |||||||
|       setHasPreview(false); |       setHasPreview(false); | ||||||
|       return undefined; |       return undefined; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // we only want to calculate this once
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   // state of playback player
 |   // state of playback player
 | ||||||
| @ -165,7 +173,7 @@ export default function DynamicVideoPlayer({ | |||||||
|   }, [timeRange]); |   }, [timeRange]); | ||||||
|   const { data: recordings } = useSWR<Recording[]>( |   const { data: recordings } = useSWR<Recording[]>( | ||||||
|     [`${camera}/recordings`, recordingParams], |     [`${camera}/recordings`, recordingParams], | ||||||
|     { revalidateOnFocus: false } |     { revalidateOnFocus: false }, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -178,13 +186,13 @@ export default function DynamicVideoPlayer({ | |||||||
|       date.getMonth() + 1 |       date.getMonth() + 1 | ||||||
|     }/${date.getDate()}/${date.getHours()}/${camera}/${timezone.replaceAll( |     }/${date.getDate()}/${date.getHours()}/${camera}/${timezone.replaceAll( | ||||||
|       "/", |       "/", | ||||||
|       "," |       ",", | ||||||
|     )}/master.m3u8`;
 |     )}/master.m3u8`;
 | ||||||
| 
 | 
 | ||||||
|     const preview = cameraPreviews.find( |     const preview = cameraPreviews.find( | ||||||
|       (preview) => |       (preview) => | ||||||
|         Math.round(preview.start) >= timeRange.start && |         Math.round(preview.start) >= timeRange.start && | ||||||
|         Math.floor(preview.end) <= timeRange.end |         Math.floor(preview.end) <= timeRange.end, | ||||||
|     ); |     ); | ||||||
|     setHasPreview(preview != undefined); |     setHasPreview(preview != undefined); | ||||||
| 
 | 
 | ||||||
| @ -193,6 +201,9 @@ export default function DynamicVideoPlayer({ | |||||||
|       playbackUri, |       playbackUri, | ||||||
|       preview, |       preview, | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     // we only want this to change when recordings update
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [controller, recordings]); |   }, [controller, recordings]); | ||||||
| 
 | 
 | ||||||
|   if (!controller) { |   if (!controller) { | ||||||
| @ -300,7 +311,7 @@ export class DynamicVideoController { | |||||||
|     previewRef: MutableRefObject<Player | undefined>, |     previewRef: MutableRefObject<Player | undefined>, | ||||||
|     annotationOffset: number, |     annotationOffset: number, | ||||||
|     setScrubbing: (isScrubbing: boolean) => void, |     setScrubbing: (isScrubbing: boolean) => void, | ||||||
|     setFocusedItem: (timeline: Timeline) => void |     setFocusedItem: (timeline: Timeline) => void, | ||||||
|   ) { |   ) { | ||||||
|     this.playerRef = playerRef; |     this.playerRef = playerRef; | ||||||
|     this.previewRef = previewRef; |     this.previewRef = previewRef; | ||||||
| @ -437,7 +448,7 @@ export class DynamicVideoController { | |||||||
|       this.timeToSeek = time; |       this.timeToSeek = time; | ||||||
|     } else { |     } else { | ||||||
|       this.previewRef.current?.currentTime( |       this.previewRef.current?.currentTime( | ||||||
|         Math.max(0, time - this.preview.start) |         Math.max(0, time - this.preview.start), | ||||||
|       ); |       ); | ||||||
|       this.seeking = true; |       this.seeking = true; | ||||||
|     } |     } | ||||||
| @ -453,7 +464,7 @@ export class DynamicVideoController { | |||||||
|       this.timeToSeek != this.previewRef.current?.currentTime() |       this.timeToSeek != this.previewRef.current?.currentTime() | ||||||
|     ) { |     ) { | ||||||
|       this.previewRef.current?.currentTime( |       this.previewRef.current?.currentTime( | ||||||
|         this.timeToSeek - this.preview.start |         this.timeToSeek - this.preview.start, | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       this.seeking = false; |       this.seeking = false; | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { baseUrl } from "@/api/baseUrl"; | import { baseUrl } from "@/api/baseUrl"; | ||||||
| import { useResizeObserver } from "@/hooks/resize-observer"; | import { useResizeObserver } from "@/hooks/resize-observer"; | ||||||
| // @ts-ignore we know this doesn't have types
 | // @ts-expect-error we know this doesn't have types
 | ||||||
| import JSMpeg from "@cycjimmy/jsmpeg-player"; | import JSMpeg from "@cycjimmy/jsmpeg-player"; | ||||||
| import { useEffect, useMemo, useRef } from "react"; | import { useEffect, useMemo, useRef } from "react"; | ||||||
| 
 | 
 | ||||||
| @ -47,10 +47,10 @@ export default function JSMpegPlayer({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return 100; |     return 100; | ||||||
|   }, [availableWidth, aspectRatio, height]); |   }, [availableWidth, aspectRatio, containerHeight, height]); | ||||||
|   const scaledWidth = useMemo( |   const scaledWidth = useMemo( | ||||||
|     () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), |     () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), | ||||||
|     [scaledHeight, aspectRatio, scrollBarWidth] |     [scaledHeight, aspectRatio, scrollBarWidth], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -62,7 +62,7 @@ export default function JSMpegPlayer({ | |||||||
|       playerRef.current, |       playerRef.current, | ||||||
|       url, |       url, | ||||||
|       {}, |       {}, | ||||||
|       { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 } |       { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const fullscreen = () => { |     const fullscreen = () => { | ||||||
| @ -79,6 +79,7 @@ export default function JSMpegPlayer({ | |||||||
|       if (playerRef.current) { |       if (playerRef.current) { | ||||||
|         try { |         try { | ||||||
|           video.destroy(); |           video.destroy(); | ||||||
|  |           // eslint-disable-next-line no-empty
 | ||||||
|         } catch (e) {} |         } catch (e) {} | ||||||
|         playerRef.current = null; |         playerRef.current = null; | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ export default function LivePlayer({ | |||||||
| 
 | 
 | ||||||
|   const cameraActive = useMemo( |   const cameraActive = useMemo( | ||||||
|     () => windowVisible && (activeMotion || activeTracking), |     () => windowVisible && (activeMotion || activeTracking), | ||||||
|     [activeMotion, activeTracking, windowVisible] |     [activeMotion, activeTracking, windowVisible], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // camera live state
 |   // camera live state
 | ||||||
| @ -56,6 +56,8 @@ export default function LivePlayer({ | |||||||
|     if (!cameraActive) { |     if (!cameraActive) { | ||||||
|       setLiveReady(false); |       setLiveReady(false); | ||||||
|     } |     } | ||||||
|  |     // live mode won't change
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [cameraActive, liveReady]); |   }, [cameraActive, liveReady]); | ||||||
| 
 | 
 | ||||||
|   const { payload: recording } = useRecordingsState(cameraConfig.name); |   const { payload: recording } = useRecordingsState(cameraConfig.name); | ||||||
| @ -76,7 +78,7 @@ export default function LivePlayer({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return 30000; |     return 30000; | ||||||
|   }, []); |   }, [liveReady, cameraActive, windowVisible]); | ||||||
| 
 | 
 | ||||||
|   if (!cameraConfig) { |   if (!cameraConfig) { | ||||||
|     return <ActivityIndicator />; |     return <ActivityIndicator />; | ||||||
|  | |||||||
| @ -37,8 +37,10 @@ function MSEPlayer({ | |||||||
|   const videoRef = useRef<HTMLVideoElement>(null); |   const videoRef = useRef<HTMLVideoElement>(null); | ||||||
|   const wsRef = useRef<WebSocket | null>(null); |   const wsRef = useRef<WebSocket | null>(null); | ||||||
|   const reconnectTIDRef = useRef<number | null>(null); |   const reconnectTIDRef = useRef<number | null>(null); | ||||||
|   const ondataRef = useRef<((data: any) => void) | null>(null); |   const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null); | ||||||
|   const onmessageRef = useRef<{ [key: string]: (msg: any) => void }>({}); |   const onmessageRef = useRef<{ | ||||||
|  |     [key: string]: (msg: { value: string; type: string }) => void; | ||||||
|  |   }>({}); | ||||||
|   const msRef = useRef<MediaSource | null>(null); |   const msRef = useRef<MediaSource | null>(null); | ||||||
| 
 | 
 | ||||||
|   const wsURL = useMemo(() => { |   const wsURL = useMemo(() => { | ||||||
| @ -49,7 +51,7 @@ function MSEPlayer({ | |||||||
|     const currentVideo = videoRef.current; |     const currentVideo = videoRef.current; | ||||||
| 
 | 
 | ||||||
|     if (currentVideo) { |     if (currentVideo) { | ||||||
|       currentVideo.play().catch((er: any) => { |       currentVideo.play().catch((er: { name: string }) => { | ||||||
|         if (er.name === "NotAllowedError" && !currentVideo.muted) { |         if (er.name === "NotAllowedError" && !currentVideo.muted) { | ||||||
|           currentVideo.muted = true; |           currentVideo.muted = true; | ||||||
|           currentVideo.play().catch(() => {}); |           currentVideo.play().catch(() => {}); | ||||||
| @ -59,16 +61,19 @@ function MSEPlayer({ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const send = useCallback( |   const send = useCallback( | ||||||
|     (value: any) => { |     (value: object) => { | ||||||
|       if (wsRef.current) wsRef.current.send(JSON.stringify(value)); |       if (wsRef.current) wsRef.current.send(JSON.stringify(value)); | ||||||
|     }, |     }, | ||||||
|     [wsRef] |     [wsRef], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const codecs = useCallback((isSupported: (type: string) => boolean) => { |   const codecs = useCallback((isSupported: (type: string) => boolean) => { | ||||||
|     return CODECS.filter((codec) => |     return CODECS.filter((codec) => | ||||||
|       isSupported(`video/mp4; codecs="${codec}"`) |       isSupported(`video/mp4; codecs="${codec}"`), | ||||||
|     ).join(); |     ).join(); | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const onConnect = useCallback(() => { |   const onConnect = useCallback(() => { | ||||||
| @ -76,6 +81,8 @@ function MSEPlayer({ | |||||||
| 
 | 
 | ||||||
|     setWsState(WebSocket.CONNECTING); |     setWsState(WebSocket.CONNECTING); | ||||||
| 
 | 
 | ||||||
|  |     // TODO may need to check this later
 | ||||||
|  |     // eslint-disable-next-line
 | ||||||
|     connectTS = Date.now(); |     connectTS = Date.now(); | ||||||
| 
 | 
 | ||||||
|     wsRef.current = new WebSocket(wsURL); |     wsRef.current = new WebSocket(wsURL); | ||||||
| @ -110,6 +117,8 @@ function MSEPlayer({ | |||||||
|     onmessageRef.current = {}; |     onmessageRef.current = {}; | ||||||
| 
 | 
 | ||||||
|     onMse(); |     onMse(); | ||||||
|  |     // only run once
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const onClose = useCallback(() => { |   const onClose = useCallback(() => { | ||||||
| @ -135,11 +144,11 @@ function MSEPlayer({ | |||||||
|         () => { |         () => { | ||||||
|           send({ |           send({ | ||||||
|             type: "mse", |             type: "mse", | ||||||
|             // @ts-ignore
 |             // @ts-expect-error for typing
 | ||||||
|             value: codecs(MediaSource.isTypeSupported), |             value: codecs(MediaSource.isTypeSupported), | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         { once: true } |         { once: true }, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       if (videoRef.current) { |       if (videoRef.current) { | ||||||
| @ -156,7 +165,7 @@ function MSEPlayer({ | |||||||
|             value: codecs(MediaSource.isTypeSupported), |             value: codecs(MediaSource.isTypeSupported), | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         { once: true } |         { once: true }, | ||||||
|       ); |       ); | ||||||
|       videoRef.current!.src = URL.createObjectURL(msRef.current!); |       videoRef.current!.src = URL.createObjectURL(msRef.current!); | ||||||
|       videoRef.current!.srcObject = null; |       videoRef.current!.srcObject = null; | ||||||
| @ -184,6 +193,7 @@ function MSEPlayer({ | |||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  |           // eslint-disable-next-line no-console
 | ||||||
|           console.debug(e); |           console.debug(e); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
| @ -201,6 +211,7 @@ function MSEPlayer({ | |||||||
|           try { |           try { | ||||||
|             sb?.appendBuffer(data); |             sb?.appendBuffer(data); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|  |             // eslint-disable-next-line no-console
 | ||||||
|             console.debug(e); |             console.debug(e); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -217,7 +228,7 @@ function MSEPlayer({ | |||||||
|     const MediaSourceConstructor = |     const MediaSourceConstructor = | ||||||
|       "ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource; |       "ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource; | ||||||
| 
 | 
 | ||||||
|     // @ts-ignore
 |     // @ts-expect-error for typing
 | ||||||
|     msRef.current = new MediaSourceConstructor(); |     msRef.current = new MediaSourceConstructor(); | ||||||
| 
 | 
 | ||||||
|     if ("hidden" in document && visibilityCheck) { |     if ("hidden" in document && visibilityCheck) { | ||||||
| @ -241,7 +252,7 @@ function MSEPlayer({ | |||||||
|             } |             } | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         { threshold: visibilityThreshold } |         { threshold: visibilityThreshold }, | ||||||
|       ); |       ); | ||||||
|       observer.observe(videoRef.current!); |       observer.observe(videoRef.current!); | ||||||
|     } |     } | ||||||
| @ -251,6 +262,8 @@ function MSEPlayer({ | |||||||
|     return () => { |     return () => { | ||||||
|       onDisconnect(); |       onDisconnect(); | ||||||
|     }; |     }; | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [playbackEnabled, onDisconnect, onConnect]); |   }, [playbackEnabled, onDisconnect, onConnect]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -78,13 +78,13 @@ export default function PreviewThumbnailPlayer({ | |||||||
|   const playingBack = useMemo(() => playback, [playback]); |   const playingBack = useMemo(() => playback, [playback]); | ||||||
| 
 | 
 | ||||||
|   const onPlayback = useCallback( |   const onPlayback = useCallback( | ||||||
|     (isHovered: Boolean) => { |     (isHovered: boolean) => { | ||||||
|       if (isHovered) { |       if (isHovered) { | ||||||
|         setHoverTimeout( |         setHoverTimeout( | ||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
|             setPlayback(true); |             setPlayback(true); | ||||||
|             setHoverTimeout(null); |             setHoverTimeout(null); | ||||||
|           }, 500) |           }, 500), | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         if (hoverTimeout) { |         if (hoverTimeout) { | ||||||
| @ -95,14 +95,17 @@ export default function PreviewThumbnailPlayer({ | |||||||
|         setProgress(0); |         setProgress(0); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [hoverTimeout, review] | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |     [hoverTimeout, review], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // date
 |   // date
 | ||||||
| 
 | 
 | ||||||
|   const formattedDate = useFormattedTimestamp( |   const formattedDate = useFormattedTimestamp( | ||||||
|     review.start_time, |     review.start_time, | ||||||
|     config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p" |     config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p", | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -134,7 +137,7 @@ export default function PreviewThumbnailPlayer({ | |||||||
|               }`}
 |               }`}
 | ||||||
|               src={`${apiHost}${review.thumb_path.replace( |               src={`${apiHost}${review.thumb_path.replace( | ||||||
|                 "/media/frigate/", |                 "/media/frigate/", | ||||||
|                 "" |                 "", | ||||||
|               )}`}
 |               )}`}
 | ||||||
|               loading={isSafari ? "eager" : "lazy"} |               loading={isSafari ? "eager" : "lazy"} | ||||||
|               onLoad={() => { |               onLoad={() => { | ||||||
| @ -215,8 +218,11 @@ function PreviewContent({ | |||||||
|     // start with a bit of padding
 |     // start with a bit of padding
 | ||||||
|     return Math.max( |     return Math.max( | ||||||
|       0, |       0, | ||||||
|       review.start_time - relevantPreview.start - PREVIEW_PADDING |       review.start_time - relevantPreview.start - PREVIEW_PADDING, | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
|   const [lastPercent, setLastPercent] = useState(0.0); |   const [lastPercent, setLastPercent] = useState(0.0); | ||||||
| 
 | 
 | ||||||
| @ -234,6 +240,9 @@ function PreviewContent({ | |||||||
|       playerRef.current.currentTime = playerStartTime; |       playerRef.current.currentTime = playerStartTime; | ||||||
|       playerRef.current.playbackRate = 8; |       playerRef.current.playbackRate = 8; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [playerRef]); |   }, [playerRef]); | ||||||
| 
 | 
 | ||||||
|   // time progress update
 |   // time progress update
 | ||||||
| @ -269,6 +278,9 @@ function PreviewContent({ | |||||||
|     } else { |     } else { | ||||||
|       setProgress(playerPercent); |       setProgress(playerPercent); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [setProgress, lastPercent]); |   }, [setProgress, lastPercent]); | ||||||
| 
 | 
 | ||||||
|   // manual playback
 |   // manual playback
 | ||||||
| @ -289,6 +301,9 @@ function PreviewContent({ | |||||||
|       } |       } | ||||||
|     }, 125); |     }, 125); | ||||||
|     return () => clearInterval(intervalId); |     return () => clearInterval(intervalId); | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [manualPlayback, playerRef]); |   }, [manualPlayback, playerRef]); | ||||||
| 
 | 
 | ||||||
|   // preview
 |   // preview
 | ||||||
| @ -333,7 +348,7 @@ function InProgressPreview({ | |||||||
|   const { data: previewFrames } = useSWR<string[]>( |   const { data: previewFrames } = useSWR<string[]>( | ||||||
|     `preview/${review.camera}/start/${Math.floor(review.start_time) - 4}/end/${ |     `preview/${review.camera}/start/${Math.floor(review.start_time) - 4}/end/${ | ||||||
|       Math.ceil(review.end_time) + 4 |       Math.ceil(review.end_time) + 4 | ||||||
|     }/frames` |     }/frames`,
 | ||||||
|   ); |   ); | ||||||
|   const [key, setKey] = useState(0); |   const [key, setKey] = useState(0); | ||||||
| 
 | 
 | ||||||
| @ -361,6 +376,9 @@ function InProgressPreview({ | |||||||
| 
 | 
 | ||||||
|       setKey(key + 1); |       setKey(key + 1); | ||||||
|     }, MIN_LOAD_TIMEOUT_MS); |     }, MIN_LOAD_TIMEOUT_MS); | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [key, previewFrames]); |   }, [key, previewFrames]); | ||||||
| 
 | 
 | ||||||
|   if (!previewFrames || previewFrames.length == 0) { |   if (!previewFrames || previewFrames.length == 0) { | ||||||
| @ -394,7 +412,7 @@ function PreviewContextItems({ | |||||||
|   const exportReview = useCallback(() => { |   const exportReview = useCallback(() => { | ||||||
|     axios.post( |     axios.post( | ||||||
|       `export/${review.camera}/start/${review.start_time}/end/${review.end_time}`, |       `export/${review.camera}/start/${review.start_time}/end/${review.end_time}`, | ||||||
|       { playback: "realtime" } |       { playback: "realtime" }, | ||||||
|     ); |     ); | ||||||
|   }, [review]); |   }, [review]); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ import Player from "video.js/dist/types/player"; | |||||||
| type VideoPlayerProps = { | type VideoPlayerProps = { | ||||||
|   children?: ReactElement | ReactElement[]; |   children?: ReactElement | ReactElement[]; | ||||||
|   options?: { |   options?: { | ||||||
|     [key: string]: any; |     [key: string]: unknown; | ||||||
|   }; |   }; | ||||||
|   seekOptions?: { |   seekOptions?: { | ||||||
|     forward?: number; |     forward?: number; | ||||||
| @ -23,7 +23,7 @@ export default function VideoPlayer({ | |||||||
|   options, |   options, | ||||||
|   seekOptions = { forward: 30, backward: 10 }, |   seekOptions = { forward: 30, backward: 10 }, | ||||||
|   remotePlayback = false, |   remotePlayback = false, | ||||||
|   onReady = (_) => {}, |   onReady = () => {}, | ||||||
|   onDispose = () => {}, |   onDispose = () => {}, | ||||||
| }: VideoPlayerProps) { | }: VideoPlayerProps) { | ||||||
|   const videoRef = useRef<HTMLDivElement | null>(null); |   const videoRef = useRef<HTMLDivElement | null>(null); | ||||||
| @ -47,7 +47,7 @@ export default function VideoPlayer({ | |||||||
|     if (!playerRef.current) { |     if (!playerRef.current) { | ||||||
|       // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
 |       // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
 | ||||||
|       const videoElement = document.createElement( |       const videoElement = document.createElement( | ||||||
|         "video-js" |         "video-js", | ||||||
|       ) as HTMLVideoElement; |       ) as HTMLVideoElement; | ||||||
|       videoElement.controls = true; |       videoElement.controls = true; | ||||||
|       videoElement.playsInline = true; |       videoElement.playsInline = true; | ||||||
| @ -62,9 +62,12 @@ export default function VideoPlayer({ | |||||||
|         { ...defaultOptions, ...options }, |         { ...defaultOptions, ...options }, | ||||||
|         () => { |         () => { | ||||||
|           onReady && onReady(player); |           onReady && onReady(player); | ||||||
|         } |         }, | ||||||
|       )); |       )); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [options, videoRef]); |   }, [options, videoRef]); | ||||||
| 
 | 
 | ||||||
|   // Dispose the Video.js player when the functional component unmounts
 |   // Dispose the Video.js player when the functional component unmounts
 | ||||||
| @ -78,6 +81,9 @@ export default function VideoPlayer({ | |||||||
|         onDispose(); |         onDispose(); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [playerRef]); |   }, [playerRef]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -58,7 +58,7 @@ export default function WebRtcPlayer({ | |||||||
|           .filter((kind) => media.indexOf(kind) >= 0) |           .filter((kind) => media.indexOf(kind) >= 0) | ||||||
|           .map( |           .map( | ||||||
|             (kind) => |             (kind) => | ||||||
|               pc.addTransceiver(kind, { direction: "recvonly" }).receiver.track |               pc.addTransceiver(kind, { direction: "recvonly" }).receiver.track, | ||||||
|           ); |           ); | ||||||
|         localTracks.push(...tracks); |         localTracks.push(...tracks); | ||||||
|       } |       } | ||||||
| @ -66,12 +66,12 @@ export default function WebRtcPlayer({ | |||||||
|       videoRef.current.srcObject = new MediaStream(localTracks); |       videoRef.current.srcObject = new MediaStream(localTracks); | ||||||
|       return pc; |       return pc; | ||||||
|     }, |     }, | ||||||
|     [videoRef] |     [videoRef], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   async function getMediaTracks( |   async function getMediaTracks( | ||||||
|     media: string, |     media: string, | ||||||
|     constraints: MediaStreamConstraints |     constraints: MediaStreamConstraints, | ||||||
|   ) { |   ) { | ||||||
|     try { |     try { | ||||||
|       const stream = |       const stream = | ||||||
| @ -126,7 +126,7 @@ export default function WebRtcPlayer({ | |||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [] |     [], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -140,7 +140,7 @@ export default function WebRtcPlayer({ | |||||||
| 
 | 
 | ||||||
|     const url = `${baseUrl.replace( |     const url = `${baseUrl.replace( | ||||||
|       /^http/, |       /^http/, | ||||||
|       "ws" |       "ws", | ||||||
|     )}live/webrtc/api/ws?src=${camera}`;
 |     )}live/webrtc/api/ws?src=${camera}`;
 | ||||||
|     const ws = new WebSocket(url); |     const ws = new WebSocket(url); | ||||||
|     const aPc = PeerConnection("video+audio"); |     const aPc = PeerConnection("video+audio"); | ||||||
|  | |||||||
| @ -52,12 +52,12 @@ export function EventReviewTimeline({ | |||||||
|   const observer = useRef<ResizeObserver | null>(null); |   const observer = useRef<ResizeObserver | null>(null); | ||||||
|   const timelineDuration = useMemo( |   const timelineDuration = useMemo( | ||||||
|     () => timelineStart - timelineEnd, |     () => timelineStart - timelineEnd, | ||||||
|     [timelineEnd, timelineStart] |     [timelineEnd, timelineStart], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( |   const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( | ||||||
|     events, |     events, | ||||||
|     segmentDuration |     segmentDuration, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { handleMouseDown, handleMouseUp, handleMouseMove } = |   const { handleMouseDown, handleMouseUp, handleMouseMove } = | ||||||
| @ -79,6 +79,7 @@ export function EventReviewTimeline({ | |||||||
| 
 | 
 | ||||||
|   function handleResize() { |   function handleResize() { | ||||||
|     // TODO: handle screen resize for mobile
 |     // TODO: handle screen resize for mobile
 | ||||||
|  |     // eslint-disable-next-line no-empty
 | ||||||
|     if (timelineRef.current && contentRef.current) { |     if (timelineRef.current && contentRef.current) { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -94,6 +95,8 @@ export function EventReviewTimeline({ | |||||||
|         observer.current?.unobserve(content); |         observer.current?.unobserve(content); | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |     // should only be calculated at beginning
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   // Generate segments for the timeline
 |   // Generate segments for the timeline
 | ||||||
| @ -119,6 +122,8 @@ export function EventReviewTimeline({ | |||||||
|         /> |         /> | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [ |   }, [ | ||||||
|     segmentDuration, |     segmentDuration, | ||||||
|     timestampSpread, |     timestampSpread, | ||||||
| @ -132,6 +137,8 @@ export function EventReviewTimeline({ | |||||||
| 
 | 
 | ||||||
|   const segments = useMemo( |   const segments = useMemo( | ||||||
|     () => generateSegments(), |     () => generateSegments(), | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|     [ |     [ | ||||||
|       segmentDuration, |       segmentDuration, | ||||||
|       timestampSpread, |       timestampSpread, | ||||||
| @ -141,7 +148,7 @@ export function EventReviewTimeline({ | |||||||
|       minimapStartTime, |       minimapStartTime, | ||||||
|       minimapEndTime, |       minimapEndTime, | ||||||
|       events, |       events, | ||||||
|     ] |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -149,7 +156,7 @@ export function EventReviewTimeline({ | |||||||
|       requestAnimationFrame(() => { |       requestAnimationFrame(() => { | ||||||
|         if (currentTimeRef.current && currentTimeSegment) { |         if (currentTimeRef.current && currentTimeSegment) { | ||||||
|           currentTimeRef.current.textContent = new Date( |           currentTimeRef.current.textContent = new Date( | ||||||
|             currentTimeSegment * 1000 |             currentTimeSegment * 1000, | ||||||
|           ).toLocaleTimeString([], { |           ).toLocaleTimeString([], { | ||||||
|             hour: "2-digit", |             hour: "2-digit", | ||||||
|             minute: "2-digit", |             minute: "2-digit", | ||||||
| @ -158,6 +165,8 @@ export function EventReviewTimeline({ | |||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [currentTimeSegment, showHandlebar]); |   }, [currentTimeSegment, showHandlebar]); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -177,7 +186,7 @@ export function EventReviewTimeline({ | |||||||
|       // Calculate the segment index corresponding to the target time
 |       // Calculate the segment index corresponding to the target time
 | ||||||
|       const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime); |       const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime); | ||||||
|       const segmentIndex = Math.ceil( |       const segmentIndex = Math.ceil( | ||||||
|         (timelineStart - alignedHandlebarTime) / segmentDuration |         (timelineStart - alignedHandlebarTime) / segmentDuration, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       // Calculate the top position based on the segment index
 |       // Calculate the top position based on the segment index
 | ||||||
| @ -193,6 +202,8 @@ export function EventReviewTimeline({ | |||||||
| 
 | 
 | ||||||
|       setCurrentTimeSegment(alignedHandlebarTime); |       setCurrentTimeSegment(alignedHandlebarTime); | ||||||
|     } |     } | ||||||
|  |     // should only be run once
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -207,6 +218,8 @@ export function EventReviewTimeline({ | |||||||
|       document.removeEventListener("mousemove", handleMouseMove); |       document.removeEventListener("mousemove", handleMouseMove); | ||||||
|       document.removeEventListener("mouseup", handleMouseUp); |       document.removeEventListener("mouseup", handleMouseUp); | ||||||
|     }; |     }; | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [ |   }, [ | ||||||
|     currentTimeSegment, |     currentTimeSegment, | ||||||
|     generateSegments, |     generateSegments, | ||||||
|  | |||||||
| @ -150,17 +150,19 @@ export function EventSegment({ | |||||||
| 
 | 
 | ||||||
|   const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( |   const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils( | ||||||
|     events, |     events, | ||||||
|     segmentDuration |     segmentDuration, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const severity = useMemo( |   const severity = useMemo( | ||||||
|     () => getSeverity(segmentTime, displaySeverityType), |     () => getSeverity(segmentTime, displaySeverityType), | ||||||
|     [getSeverity, segmentTime] |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |     [getSeverity, segmentTime], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const reviewed = useMemo( |   const reviewed = useMemo( | ||||||
|     () => getReviewed(segmentTime), |     () => getReviewed(segmentTime), | ||||||
|     [getReviewed, segmentTime] |     [getReviewed, segmentTime], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { |   const { | ||||||
| @ -170,7 +172,7 @@ export function EventSegment({ | |||||||
|     roundBottomSecondary, |     roundBottomSecondary, | ||||||
|   } = useMemo( |   } = useMemo( | ||||||
|     () => shouldShowRoundedCorners(segmentTime), |     () => shouldShowRoundedCorners(segmentTime), | ||||||
|     [shouldShowRoundedCorners, segmentTime] |     [shouldShowRoundedCorners, segmentTime], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const startTimestamp = useMemo(() => { |   const startTimestamp = useMemo(() => { | ||||||
| @ -178,6 +180,8 @@ export function EventSegment({ | |||||||
|     if (eventStart) { |     if (eventStart) { | ||||||
|       return alignStartDateToTimeline(eventStart); |       return alignStartDateToTimeline(eventStart); | ||||||
|     } |     } | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [getEventStart, segmentTime]); |   }, [getEventStart, segmentTime]); | ||||||
| 
 | 
 | ||||||
|   const apiHost = useApiHost(); |   const apiHost = useApiHost(); | ||||||
| @ -191,11 +195,11 @@ export function EventSegment({ | |||||||
| 
 | 
 | ||||||
|   const alignedMinimapStartTime = useMemo( |   const alignedMinimapStartTime = useMemo( | ||||||
|     () => alignStartDateToTimeline(minimapStartTime ?? 0), |     () => alignStartDateToTimeline(minimapStartTime ?? 0), | ||||||
|     [minimapStartTime, alignStartDateToTimeline] |     [minimapStartTime, alignStartDateToTimeline], | ||||||
|   ); |   ); | ||||||
|   const alignedMinimapEndTime = useMemo( |   const alignedMinimapEndTime = useMemo( | ||||||
|     () => alignEndDateToTimeline(minimapEndTime ?? 0), |     () => alignEndDateToTimeline(minimapEndTime ?? 0), | ||||||
|     [minimapEndTime, alignEndDateToTimeline] |     [minimapEndTime, alignEndDateToTimeline], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const isInMinimapRange = useMemo(() => { |   const isInMinimapRange = useMemo(() => { | ||||||
| @ -236,6 +240,8 @@ export function EventSegment({ | |||||||
|     if (firstSegment && showMinimap && isFirstSegmentInMinimap) { |     if (firstSegment && showMinimap && isFirstSegmentInMinimap) { | ||||||
|       debounceScrollIntoView(firstSegment); |       debounceScrollIntoView(firstSegment); | ||||||
|     } |     } | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); |   }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); | ||||||
| 
 | 
 | ||||||
|   const segmentClasses = `h-2 relative w-full ${ |   const segmentClasses = `h-2 relative w-full ${ | ||||||
| @ -267,13 +273,13 @@ export function EventSegment({ | |||||||
|   const segmentClick = useCallback(() => { |   const segmentClick = useCallback(() => { | ||||||
|     if (contentRef.current && startTimestamp) { |     if (contentRef.current && startTimestamp) { | ||||||
|       const element = contentRef.current.querySelector( |       const element = contentRef.current.querySelector( | ||||||
|         `[data-segment-start="${startTimestamp - segmentDuration}"]` |         `[data-segment-start="${startTimestamp - segmentDuration}"]`, | ||||||
|       ); |       ); | ||||||
|       if (element instanceof HTMLElement) { |       if (element instanceof HTMLElement) { | ||||||
|         debounceScrollIntoView(element); |         debounceScrollIntoView(element); | ||||||
|         element.classList.add( |         element.classList.add( | ||||||
|           `outline-severity_${severityType}`, |           `outline-severity_${severityType}`, | ||||||
|           `shadow-severity_${severityType}` |           `shadow-severity_${severityType}`, | ||||||
|         ); |         ); | ||||||
|         element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); |         element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); | ||||||
|         element.classList.remove("outline-0", "shadow-none"); |         element.classList.remove("outline-0", "shadow-none"); | ||||||
| @ -285,6 +291,8 @@ export function EventSegment({ | |||||||
|         }, 3000); |         }, 3000); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [startTimestamp]); |   }, [startTimestamp]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -67,6 +67,7 @@ export function ThemeProvider({ | |||||||
|       const storedData = JSON.parse(localStorage.getItem(storageKey) || "{}"); |       const storedData = JSON.parse(localStorage.getItem(storageKey) || "{}"); | ||||||
|       return storedData.theme || defaultTheme; |       return storedData.theme || defaultTheme; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |       // eslint-disable-next-line no-console
 | ||||||
|       console.error("Error parsing theme data from storage:", error); |       console.error("Error parsing theme data from storage:", error); | ||||||
|       return defaultTheme; |       return defaultTheme; | ||||||
|     } |     } | ||||||
| @ -79,6 +80,7 @@ export function ThemeProvider({ | |||||||
|         ? defaultColorScheme |         ? defaultColorScheme | ||||||
|         : storedData.colorScheme || defaultColorScheme; |         : storedData.colorScheme || defaultColorScheme; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|  |       // eslint-disable-next-line no-console
 | ||||||
|       console.error("Error parsing color scheme data from storage:", error); |       console.error("Error parsing color scheme data from storage:", error); | ||||||
|       return defaultColorScheme; |       return defaultColorScheme; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1 +1 @@ | |||||||
| export const ENV = import.meta.env.MODE; | export const ENV = import.meta.env.MODE; | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ export function useResizeObserver(...refs: MutableRefObject<Element | null>[]) { | |||||||
|       height: 0, |       height: 0, | ||||||
|       x: -Infinity, |       x: -Infinity, | ||||||
|       y: -Infinity, |       y: -Infinity, | ||||||
|     }) |     }), | ||||||
|   ); |   ); | ||||||
|   const resizeObserver = useMemo( |   const resizeObserver = useMemo( | ||||||
|     () => |     () => | ||||||
| @ -16,7 +16,7 @@ export function useResizeObserver(...refs: MutableRefObject<Element | null>[]) { | |||||||
|           setDimensions(entries.map((entry) => entry.contentRect)); |           setDimensions(entries.map((entry) => entry.contentRect)); | ||||||
|         }); |         }); | ||||||
|       }), |       }), | ||||||
|     [] |     [], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
|  | import { FilterType } from "@/types/filter"; | ||||||
| import { useMemo, useState } from "react"; | import { useMemo, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| type useApiFilterReturn<F extends FilterType> = [ | type useApiFilterReturn<F extends FilterType> = [ | ||||||
|   filter: F | undefined, |   filter: F | undefined, | ||||||
|   setFilter: (filter: F) => void, |   setFilter: (filter: F) => void, | ||||||
|   searchParams: { |   searchParams: { | ||||||
|  |     // accept any type for a filter
 | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|     [key: string]: any; |     [key: string]: any; | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  | |||||||
| @ -13,12 +13,12 @@ type useCameraActivityReturn = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default function useCameraActivity( | export default function useCameraActivity( | ||||||
|   camera: CameraConfig |   camera: CameraConfig, | ||||||
| ): useCameraActivityReturn { | ): useCameraActivityReturn { | ||||||
|   const [activeObjects, setActiveObjects] = useState<string[]>([]); |   const [activeObjects, setActiveObjects] = useState<string[]>([]); | ||||||
|   const hasActiveObjects = useMemo( |   const hasActiveObjects = useMemo( | ||||||
|     () => activeObjects.length > 0, |     () => activeObjects.length > 0, | ||||||
|     [activeObjects] |     [activeObjects], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { payload: detectingMotion } = useMotionActivity(camera.name); |   const { payload: detectingMotion } = useMotionActivity(camera.name); | ||||||
| @ -56,7 +56,7 @@ export default function useCameraActivity( | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, [event, activeObjects]); |   }, [camera, event, activeObjects]); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     activeTracking: hasActiveObjects, |     activeTracking: hasActiveObjects, | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ import { LivePlayerMode } from "@/types/live"; | |||||||
| 
 | 
 | ||||||
| export default function useCameraLiveMode( | export default function useCameraLiveMode( | ||||||
|   cameraConfig: CameraConfig, |   cameraConfig: CameraConfig, | ||||||
|   preferredMode?: string |   preferredMode?: string, | ||||||
| ): LivePlayerMode | undefined { | ): LivePlayerMode | undefined { | ||||||
|   const { data: config } = useSWR<FrigateConfig>("config"); |   const { data: config } = useSWR<FrigateConfig>("config"); | ||||||
| 
 | 
 | ||||||
| @ -18,7 +18,7 @@ export default function useCameraLiveMode( | |||||||
|     return ( |     return ( | ||||||
|       cameraConfig && |       cameraConfig && | ||||||
|       Object.keys(config.go2rtc.streams || {}).includes( |       Object.keys(config.go2rtc.streams || {}).includes( | ||||||
|         cameraConfig.live.stream_name |         cameraConfig.live.stream_name, | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|   }, [config, cameraConfig]); |   }, [config, cameraConfig]); | ||||||
| @ -32,10 +32,12 @@ export default function useCameraLiveMode( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return undefined; |     return undefined; | ||||||
|  |     // config will be updated if camera config is updated
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [cameraConfig, restreamEnabled]); |   }, [cameraConfig, restreamEnabled]); | ||||||
|   const [viewSource] = usePersistence<LivePlayerMode>( |   const [viewSource] = usePersistence<LivePlayerMode>( | ||||||
|     `${cameraConfig.name}-source`, |     `${cameraConfig.name}-source`, | ||||||
|     defaultLiveMode |     defaultLiveMode, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   if ( |   if ( | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import { ReviewSegment } from "@/types/review"; | |||||||
| 
 | 
 | ||||||
| export const useEventUtils = ( | export const useEventUtils = ( | ||||||
|   events: ReviewSegment[], |   events: ReviewSegment[], | ||||||
|   segmentDuration: number |   segmentDuration: number, | ||||||
| ) => { | ) => { | ||||||
|   const isStartOfEvent = useCallback( |   const isStartOfEvent = useCallback( | ||||||
|     (time: number): boolean => { |     (time: number): boolean => { | ||||||
| @ -12,7 +12,9 @@ export const useEventUtils = ( | |||||||
|         return time >= segmentStart && time < segmentStart + segmentDuration; |         return time >= segmentStart && time < segmentStart + segmentDuration; | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [events, segmentDuration] |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |     [events, segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const isEndOfEvent = useCallback( |   const isEndOfEvent = useCallback( | ||||||
| @ -25,21 +27,23 @@ export const useEventUtils = ( | |||||||
|         return false; |         return false; | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [events, segmentDuration] |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |     [events, segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getSegmentStart = useCallback( |   const getSegmentStart = useCallback( | ||||||
|     (time: number): number => { |     (time: number): number => { | ||||||
|       return Math.floor(time / segmentDuration) * segmentDuration; |       return Math.floor(time / segmentDuration) * segmentDuration; | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getSegmentEnd = useCallback( |   const getSegmentEnd = useCallback( | ||||||
|     (time: number): number => { |     (time: number): number => { | ||||||
|       return Math.ceil(time / segmentDuration) * segmentDuration; |       return Math.ceil(time / segmentDuration) * segmentDuration; | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const alignEndDateToTimeline = useCallback( |   const alignEndDateToTimeline = useCallback( | ||||||
| @ -48,16 +52,16 @@ export const useEventUtils = ( | |||||||
|       const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; |       const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; | ||||||
|       return time + adjustment; |       return time + adjustment; | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const alignStartDateToTimeline = useCallback( |   const alignStartDateToTimeline = useCallback( | ||||||
|     (time: number): number => { |     (time: number): number => { | ||||||
|       const remainder = time % segmentDuration; |       const remainder = time % segmentDuration; | ||||||
|       const adjustment = remainder === 0 ? 0 : -(remainder); |       const adjustment = remainder === 0 ? 0 : -remainder; | ||||||
|       return time + adjustment; |       return time + adjustment; | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|  | |||||||
| @ -37,7 +37,7 @@ function useDraggableHandler({ | |||||||
|       e.stopPropagation(); |       e.stopPropagation(); | ||||||
|       setIsDragging(true); |       setIsDragging(true); | ||||||
|     }, |     }, | ||||||
|     [setIsDragging] |     [setIsDragging], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const handleMouseUp = useCallback( |   const handleMouseUp = useCallback( | ||||||
| @ -48,7 +48,7 @@ function useDraggableHandler({ | |||||||
|         setIsDragging(false); |         setIsDragging(false); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [isDragging, setIsDragging] |     [isDragging, setIsDragging], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const handleMouseMove = useCallback( |   const handleMouseMove = useCallback( | ||||||
| @ -90,13 +90,13 @@ function useDraggableHandler({ | |||||||
|           visibleTimelineHeight - timelineTop + parentScrollTop, |           visibleTimelineHeight - timelineTop + parentScrollTop, | ||||||
|           Math.max( |           Math.max( | ||||||
|             segmentHeight + scrolled, |             segmentHeight + scrolled, | ||||||
|             e.clientY - timelineTop + parentScrollTop |             e.clientY - timelineTop + parentScrollTop, | ||||||
|           ) |           ), | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const segmentIndex = Math.floor(newHandlePosition / segmentHeight); |         const segmentIndex = Math.floor(newHandlePosition / segmentHeight); | ||||||
|         const segmentStartTime = alignStartDateToTimeline( |         const segmentStartTime = alignStartDateToTimeline( | ||||||
|           timelineStart - segmentIndex * segmentDuration |           timelineStart - segmentIndex * segmentDuration, | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (showHandlebar) { |         if (showHandlebar) { | ||||||
| @ -105,7 +105,7 @@ function useDraggableHandler({ | |||||||
|             thumb.style.top = `${newHandlePosition - segmentHeight}px`; |             thumb.style.top = `${newHandlePosition - segmentHeight}px`; | ||||||
|             if (currentTimeRef.current) { |             if (currentTimeRef.current) { | ||||||
|               currentTimeRef.current.textContent = new Date( |               currentTimeRef.current.textContent = new Date( | ||||||
|                 segmentStartTime * 1000 |                 segmentStartTime * 1000, | ||||||
|               ).toLocaleTimeString([], { |               ).toLocaleTimeString([], { | ||||||
|                 hour: "2-digit", |                 hour: "2-digit", | ||||||
|                 minute: "2-digit", |                 minute: "2-digit", | ||||||
| @ -116,12 +116,14 @@ function useDraggableHandler({ | |||||||
|           if (setHandlebarTime) { |           if (setHandlebarTime) { | ||||||
|             setHandlebarTime( |             setHandlebarTime( | ||||||
|               timelineStart - |               timelineStart - | ||||||
|                 (newHandlePosition / segmentHeight) * segmentDuration |                 (newHandlePosition / segmentHeight) * segmentDuration, | ||||||
|             ); |             ); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|     [ |     [ | ||||||
|       isDragging, |       isDragging, | ||||||
|       contentRef, |       contentRef, | ||||||
| @ -129,7 +131,7 @@ function useDraggableHandler({ | |||||||
|       showHandlebar, |       showHandlebar, | ||||||
|       timelineDuration, |       timelineDuration, | ||||||
|       timelineStart, |       timelineStart, | ||||||
|     ] |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -141,6 +143,8 @@ function useDraggableHandler({ | |||||||
|         block: "center", |         block: "center", | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |     // temporary until behavior is decided
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   return { handleMouseDown, handleMouseUp, handleMouseMove }; |   return { handleMouseDown, handleMouseUp, handleMouseMove }; | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { useCallback, useEffect } from "react"; | |||||||
| 
 | 
 | ||||||
| export default function useKeyboardListener( | export default function useKeyboardListener( | ||||||
|   keys: string[], |   keys: string[], | ||||||
|   listener: (key: string, down: boolean, repeat: boolean) => void |   listener: (key: string, down: boolean, repeat: boolean) => void, | ||||||
| ) { | ) { | ||||||
|   const keyDownListener = useCallback( |   const keyDownListener = useCallback( | ||||||
|     (e: KeyboardEvent) => { |     (e: KeyboardEvent) => { | ||||||
| @ -15,7 +15,7 @@ export default function useKeyboardListener( | |||||||
|         listener(e.key, true, e.repeat); |         listener(e.key, true, e.repeat); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [listener] |     [keys, listener], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const keyUpListener = useCallback( |   const keyUpListener = useCallback( | ||||||
| @ -29,7 +29,7 @@ export default function useKeyboardListener( | |||||||
|         listener(e.key, false, false); |         listener(e.key, false, false); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [listener] |     [keys, listener], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -39,5 +39,5 @@ export default function useKeyboardListener( | |||||||
|       document.removeEventListener("keydown", keyDownListener); |       document.removeEventListener("keydown", keyDownListener); | ||||||
|       document.removeEventListener("keyup", keyUpListener); |       document.removeEventListener("keyup", keyUpListener); | ||||||
|     }; |     }; | ||||||
|   }, [listener]); |   }, [listener, keyDownListener, keyUpListener]); | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,7 +12,9 @@ export default function useOverlayState(key: string) { | |||||||
|       newLocationState[key] = value; |       newLocationState[key] = value; | ||||||
|       navigate(location.pathname, { state: newLocationState }); |       navigate(location.pathname, { state: newLocationState }); | ||||||
|     }, |     }, | ||||||
|     [navigate] |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |     [key, navigate], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const overlayStateValue = location.state && location.state[key]; |   const overlayStateValue = location.state && location.state[key]; | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ type usePersistenceReturn<S> = [ | |||||||
| 
 | 
 | ||||||
| export function usePersistence<S>( | export function usePersistence<S>( | ||||||
|   key: string, |   key: string, | ||||||
|   defaultValue: S | undefined = undefined |   defaultValue: S | undefined = undefined, | ||||||
| ): usePersistenceReturn<S> { | ): usePersistenceReturn<S> { | ||||||
|   const [value, setInternalValue] = useState<S | undefined>(defaultValue); |   const [value, setInternalValue] = useState<S | undefined>(defaultValue); | ||||||
|   const [loaded, setLoaded] = useState<boolean>(false); |   const [loaded, setLoaded] = useState<boolean>(false); | ||||||
| @ -23,7 +23,7 @@ export function usePersistence<S>( | |||||||
| 
 | 
 | ||||||
|       update(); |       update(); | ||||||
|     }, |     }, | ||||||
|     [key] |     [key], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  | |||||||
| @ -4,13 +4,13 @@ import { ReviewSegment } from "@/types/review"; | |||||||
| export const useSegmentUtils = ( | export const useSegmentUtils = ( | ||||||
|   segmentDuration: number, |   segmentDuration: number, | ||||||
|   events: ReviewSegment[], |   events: ReviewSegment[], | ||||||
|   severityType: string |   severityType: string, | ||||||
| ) => { | ) => { | ||||||
|   const getSegmentStart = useCallback( |   const getSegmentStart = useCallback( | ||||||
|     (time: number): number => { |     (time: number): number => { | ||||||
|       return Math.floor(time / segmentDuration) * segmentDuration; |       return Math.floor(time / segmentDuration) * segmentDuration; | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getSegmentEnd = useCallback( |   const getSegmentEnd = useCallback( | ||||||
| @ -23,7 +23,7 @@ export const useSegmentUtils = ( | |||||||
|         return Date.now() / 1000 + segmentDuration; |         return Date.now() / 1000 + segmentDuration; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [segmentDuration] |     [segmentDuration], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const mapSeverityToNumber = useCallback((severity: string): number => { |   const mapSeverityToNumber = useCallback((severity: string): number => { | ||||||
| @ -41,7 +41,7 @@ export const useSegmentUtils = ( | |||||||
| 
 | 
 | ||||||
|   const displaySeverityType = useMemo( |   const displaySeverityType = useMemo( | ||||||
|     () => mapSeverityToNumber(severityType ?? ""), |     () => mapSeverityToNumber(severityType ?? ""), | ||||||
|     [severityType] |     [mapSeverityToNumber, severityType], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getSeverity = useCallback( |   const getSeverity = useCallback( | ||||||
| @ -54,7 +54,7 @@ export const useSegmentUtils = ( | |||||||
| 
 | 
 | ||||||
|       if (activeEvents?.length === 0) return [0]; |       if (activeEvents?.length === 0) return [0]; | ||||||
|       const severityValues = activeEvents.map((event) => |       const severityValues = activeEvents.map((event) => | ||||||
|         mapSeverityToNumber(event.severity) |         mapSeverityToNumber(event.severity), | ||||||
|       ); |       ); | ||||||
|       const highestSeverityValue = Math.max(...severityValues); |       const highestSeverityValue = Math.max(...severityValues); | ||||||
| 
 | 
 | ||||||
| @ -67,7 +67,7 @@ export const useSegmentUtils = ( | |||||||
|         return [highestSeverityValue]; |         return [highestSeverityValue]; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber] |     [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getReviewed = useCallback( |   const getReviewed = useCallback( | ||||||
| @ -80,12 +80,12 @@ export const useSegmentUtils = ( | |||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     [events, getSegmentStart, getSegmentEnd] |     [events, getSegmentStart, getSegmentEnd], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const shouldShowRoundedCorners = useCallback( |   const shouldShowRoundedCorners = useCallback( | ||||||
|     ( |     ( | ||||||
|       segmentTime: number |       segmentTime: number, | ||||||
|     ): { |     ): { | ||||||
|       roundTopPrimary: boolean; |       roundTopPrimary: boolean; | ||||||
|       roundBottomPrimary: boolean; |       roundBottomPrimary: boolean; | ||||||
| @ -163,7 +163,7 @@ export const useSegmentUtils = ( | |||||||
|         roundBottomSecondary, |         roundBottomSecondary, | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|     [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] |     [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getEventStart = useCallback( |   const getEventStart = useCallback( | ||||||
| @ -178,7 +178,7 @@ export const useSegmentUtils = ( | |||||||
| 
 | 
 | ||||||
|       return matchingEvent?.start_time ?? 0; |       return matchingEvent?.start_time ?? 0; | ||||||
|     }, |     }, | ||||||
|     [events, getSegmentStart, getSegmentEnd, severityType] |     [events, getSegmentStart, getSegmentEnd, severityType], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const getEventThumbnail = useCallback( |   const getEventThumbnail = useCallback( | ||||||
| @ -193,7 +193,7 @@ export const useSegmentUtils = ( | |||||||
| 
 | 
 | ||||||
|       return matchingEvent?.thumb_path ?? ""; |       return matchingEvent?.thumb_path ?? ""; | ||||||
|     }, |     }, | ||||||
|     [events, getSegmentStart, getSegmentEnd, severityType] |     [events, getSegmentStart, getSegmentEnd, severityType], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
| @ -204,6 +204,6 @@ export const useSegmentUtils = ( | |||||||
|     getReviewed, |     getReviewed, | ||||||
|     shouldShowRoundedCorners, |     shouldShowRoundedCorners, | ||||||
|     getEventStart, |     getEventStart, | ||||||
|     getEventThumbnail |     getEventThumbnail, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,25 +1,25 @@ | |||||||
| const formatter = new Intl.RelativeTimeFormat(undefined, { | const formatter = new Intl.RelativeTimeFormat(undefined, { | ||||||
|     numeric: "always", |   numeric: "always", | ||||||
|   }) | }); | ||||||
| 
 | 
 | ||||||
|   const DIVISIONS: { amount: number; name: Intl.RelativeTimeFormatUnit }[] = [ | const DIVISIONS: { amount: number; name: Intl.RelativeTimeFormatUnit }[] = [ | ||||||
|     { amount: 60, name: "seconds" }, |   { amount: 60, name: "seconds" }, | ||||||
|     { amount: 60, name: "minutes" }, |   { amount: 60, name: "minutes" }, | ||||||
|     { amount: 24, name: "hours" }, |   { amount: 24, name: "hours" }, | ||||||
|     { amount: 7, name: "days" }, |   { amount: 7, name: "days" }, | ||||||
|     { amount: 4.34524, name: "weeks" }, |   { amount: 4.34524, name: "weeks" }, | ||||||
|     { amount: 12, name: "months" }, |   { amount: 12, name: "months" }, | ||||||
|     { amount: Number.POSITIVE_INFINITY, name: "years" }, |   { amount: Number.POSITIVE_INFINITY, name: "years" }, | ||||||
|   ] | ]; | ||||||
| 
 | 
 | ||||||
|   export function formatTimeAgo(date: Date) { | export function formatTimeAgo(date: Date) { | ||||||
|     let duration = (date.getTime() - new Date().getTime()) / 1000 |   let duration = (date.getTime() - new Date().getTime()) / 1000; | ||||||
| 
 | 
 | ||||||
|     for (let i = 0; i < DIVISIONS.length; i++) { |   for (let i = 0; i < DIVISIONS.length; i++) { | ||||||
|       const division = DIVISIONS[i] |     const division = DIVISIONS[i]; | ||||||
|       if (Math.abs(duration) < division.amount) { |     if (Math.abs(duration) < division.amount) { | ||||||
|         return formatter.format(Math.round(duration), division.name) |       return formatter.format(Math.round(duration), division.name); | ||||||
|       } |  | ||||||
|       duration /= division.amount |  | ||||||
|     } |     } | ||||||
|   } |     duration /= division.amount; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { type ClassValue, clsx } from "clsx" | import { type ClassValue, clsx } from "clsx"; | ||||||
| import { twMerge } from "tailwind-merge" | import { twMerge } from "tailwind-merge"; | ||||||
| 
 | 
 | ||||||
| export function cn(...inputs: ClassValue[]) { | export function cn(...inputs: ClassValue[]) { | ||||||
|   return twMerge(clsx(inputs)) |   return twMerge(clsx(inputs)); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| import React from 'react' | import React from "react"; | ||||||
| import ReactDOM from 'react-dom/client' | import ReactDOM from "react-dom/client"; | ||||||
| import App from './App.tsx' | import App from "./App.tsx"; | ||||||
| import './index.css' | import "./index.css"; | ||||||
| 
 | 
 | ||||||
| ReactDOM.createRoot(document.getElementById('root')!).render( | ReactDOM.createRoot(document.getElementById("root")!).render( | ||||||
|   <React.StrictMode> |   <React.StrictMode> | ||||||
|     <App /> |     <App /> | ||||||
|   </React.StrictMode>, |   </React.StrictMode>, | ||||||
| ) | ); | ||||||
|  | |||||||
| @ -38,7 +38,7 @@ function ConfigEditor() { | |||||||
|           editorRef.current.getValue(), |           editorRef.current.getValue(), | ||||||
|           { |           { | ||||||
|             headers: { "Content-Type": "text/plain" }, |             headers: { "Content-Type": "text/plain" }, | ||||||
|           } |           }, | ||||||
|         ) |         ) | ||||||
|         .then((response) => { |         .then((response) => { | ||||||
|           if (response.status === 200) { |           if (response.status === 200) { | ||||||
| @ -56,7 +56,7 @@ function ConfigEditor() { | |||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|     [editorRef] |     [editorRef], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const handleCopyConfig = useCallback(async () => { |   const handleCopyConfig = useCallback(async () => { | ||||||
| @ -127,24 +127,20 @@ function ConfigEditor() { | |||||||
|       <div className="lg:flex justify-between mr-1"> |       <div className="lg:flex justify-between mr-1"> | ||||||
|         <Heading as="h2">Config</Heading> |         <Heading as="h2">Config</Heading> | ||||||
|         <div> |         <div> | ||||||
|           <Button |           <Button size="sm" className="mx-1" onClick={() => handleCopyConfig()}> | ||||||
|             size="sm" |  | ||||||
|             className="mx-1" |  | ||||||
|             onClick={(_) => handleCopyConfig()} |  | ||||||
|           > |  | ||||||
|             Copy Config |             Copy Config | ||||||
|           </Button> |           </Button> | ||||||
|           <Button |           <Button | ||||||
|             size="sm" |             size="sm" | ||||||
|             className="mx-1" |             className="mx-1" | ||||||
|             onClick={(_) => onHandleSaveConfig("restart")} |             onClick={() => onHandleSaveConfig("restart")} | ||||||
|           > |           > | ||||||
|             Save & Restart |             Save & Restart | ||||||
|           </Button> |           </Button> | ||||||
|           <Button |           <Button | ||||||
|             size="sm" |             size="sm" | ||||||
|             className="mx-1" |             className="mx-1" | ||||||
|             onClick={(_) => onHandleSaveConfig("saveonly")} |             onClick={() => onHandleSaveConfig("saveonly")} | ||||||
|           > |           > | ||||||
|             Save Only |             Save Only | ||||||
|           </Button> |           </Button> | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import useApiFilter from "@/hooks/use-api-filter"; | import useApiFilter from "@/hooks/use-api-filter"; | ||||||
| import useOverlayState from "@/hooks/use-overlay-state"; | import useOverlayState from "@/hooks/use-overlay-state"; | ||||||
|  | import { Preview } from "@/types/preview"; | ||||||
| import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; | import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; | ||||||
| import DesktopRecordingView from "@/views/events/DesktopRecordingView"; | import DesktopRecordingView from "@/views/events/DesktopRecordingView"; | ||||||
| import EventView from "@/views/events/EventView"; | import EventView from "@/views/events/EventView"; | ||||||
| @ -24,6 +25,8 @@ export default function Events() { | |||||||
|   const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { |   const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { | ||||||
|     setSize(1); |     setSize(1); | ||||||
|     setReviewFilter(newFilter); |     setReviewFilter(newFilter); | ||||||
|  |     // we don't want this updating
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   // review paging
 |   // review paging
 | ||||||
| @ -41,9 +44,9 @@ export default function Events() { | |||||||
|       before: Math.floor(reviewSearchParams["before"]), |       before: Math.floor(reviewSearchParams["before"]), | ||||||
|       after: Math.floor(reviewSearchParams["after"]), |       after: Math.floor(reviewSearchParams["after"]), | ||||||
|     }; |     }; | ||||||
|   }, [reviewSearchParams]); |   }, [last24Hours, reviewSearchParams]); | ||||||
| 
 | 
 | ||||||
|   const reviewSegmentFetcher = useCallback((key: any) => { |   const reviewSegmentFetcher = useCallback((key: Array<string> | string) => { | ||||||
|     const [path, params] = Array.isArray(key) ? key : [key, undefined]; |     const [path, params] = Array.isArray(key) ? key : [key, undefined]; | ||||||
|     return axios.get(path, { params }).then((res) => res.data); |     return axios.get(path, { params }).then((res) => res.data); | ||||||
|   }, []); |   }, []); | ||||||
| @ -74,7 +77,7 @@ export default function Events() { | |||||||
|       }; |       }; | ||||||
|       return ["review", params]; |       return ["review", params]; | ||||||
|     }, |     }, | ||||||
|     [reviewSearchParams, last24Hours] |     [reviewSearchParams, last24Hours], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const { |   const { | ||||||
| @ -90,7 +93,7 @@ export default function Events() { | |||||||
| 
 | 
 | ||||||
|   const isDone = useMemo( |   const isDone = useMemo( | ||||||
|     () => (reviewPages?.at(-1)?.length ?? 0) < API_LIMIT, |     () => (reviewPages?.at(-1)?.length ?? 0) < API_LIMIT, | ||||||
|     [reviewPages] |     [reviewPages], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); |   const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); | ||||||
| @ -103,7 +106,7 @@ export default function Events() { | |||||||
|     if ( |     if ( | ||||||
|       !reviewPages || |       !reviewPages || | ||||||
|       reviewPages.length == 0 || |       reviewPages.length == 0 || | ||||||
|       reviewPages.at(-1)!!.length == 0 |       reviewPages.at(-1)?.length == 0 | ||||||
|     ) { |     ) { | ||||||
|       return undefined; |       return undefined; | ||||||
|     } |     } | ||||||
| @ -111,7 +114,7 @@ export default function Events() { | |||||||
|     const startDate = new Date(); |     const startDate = new Date(); | ||||||
|     startDate.setMinutes(0, 0, 0); |     startDate.setMinutes(0, 0, 0); | ||||||
| 
 | 
 | ||||||
|     const endDate = new Date(reviewPages.at(-1)!!.at(-1)!!.end_time); |     const endDate = new Date(reviewPages.at(-1)?.at(-1)?.end_time || 0); | ||||||
|     endDate.setHours(0, 0, 0, 0); |     endDate.setHours(0, 0, 0, 0); | ||||||
|     return { |     return { | ||||||
|       start: startDate.getTime() / 1000, |       start: startDate.getTime() / 1000, | ||||||
| @ -122,7 +125,7 @@ export default function Events() { | |||||||
|     previewTimes |     previewTimes | ||||||
|       ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` |       ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` | ||||||
|       : null, |       : null, | ||||||
|     { revalidateOnFocus: false } |     { revalidateOnFocus: false }, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // review status
 |   // review status
 | ||||||
| @ -156,11 +159,11 @@ export default function Events() { | |||||||
| 
 | 
 | ||||||
|             return newData; |             return newData; | ||||||
|           }, |           }, | ||||||
|           { revalidate: false, populateCache: true } |           { revalidate: false, populateCache: true }, | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [updateSegments] |     [updateSegments], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // selected items
 |   // selected items
 | ||||||
| @ -176,7 +179,7 @@ export default function Events() { | |||||||
| 
 | 
 | ||||||
|     const allReviews = reviewPages.flat(); |     const allReviews = reviewPages.flat(); | ||||||
|     const selectedReview = allReviews.find( |     const selectedReview = allReviews.find( | ||||||
|       (item) => item.id == selectedReviewId |       (item) => item.id == selectedReviewId, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     if (!selectedReview) { |     if (!selectedReview) { | ||||||
| @ -186,12 +189,15 @@ export default function Events() { | |||||||
|     return { |     return { | ||||||
|       selected: selectedReview, |       selected: selectedReview, | ||||||
|       cameraSegments: allReviews.filter( |       cameraSegments: allReviews.filter( | ||||||
|         (seg) => seg.camera == selectedReview.camera |         (seg) => seg.camera == selectedReview.camera, | ||||||
|       ), |       ), | ||||||
|       cameraPreviews: allPreviews?.filter( |       cameraPreviews: allPreviews?.filter( | ||||||
|         (seg) => seg.camera == selectedReview.camera |         (seg) => seg.camera == selectedReview.camera, | ||||||
|       ), |       ), | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|  |     // previews will not update after item is selected
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [selectedReviewId, reviewPages]); |   }, [selectedReviewId, reviewPages]); | ||||||
| 
 | 
 | ||||||
|   if (selectedData) { |   if (selectedData) { | ||||||
|  | |||||||
| @ -51,7 +51,7 @@ function Export() { | |||||||
|   const { data: config } = useSWR<FrigateConfig>("config"); |   const { data: config } = useSWR<FrigateConfig>("config"); | ||||||
|   const { data: exports, mutate } = useSWR<ExportItem[]>( |   const { data: exports, mutate } = useSWR<ExportItem[]>( | ||||||
|     "exports/", |     "exports/", | ||||||
|     (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data) |     (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data), | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // Export States
 |   // Export States
 | ||||||
| @ -96,7 +96,7 @@ function Export() { | |||||||
|       parseInt(startHour), |       parseInt(startHour), | ||||||
|       parseInt(startMin), |       parseInt(startMin), | ||||||
|       parseInt(startSec), |       parseInt(startSec), | ||||||
|       0 |       0, | ||||||
|     ); |     ); | ||||||
|     const start = startDate.getTime() / 1000; |     const start = startDate.getTime() / 1000; | ||||||
|     const endDate = new Date((date.to || date.from).getTime()); |     const endDate = new Date((date.to || date.from).getTime()); | ||||||
| @ -117,7 +117,7 @@ function Export() { | |||||||
|         if (response.status == 200) { |         if (response.status == 200) { | ||||||
|           toast.success( |           toast.success( | ||||||
|             "Successfully started export. View the file in the /exports folder.", |             "Successfully started export. View the file in the /exports folder.", | ||||||
|             { position: "top-center" } |             { position: "top-center" }, | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -127,7 +127,7 @@ function Export() { | |||||||
|         if (error.response?.data?.message) { |         if (error.response?.data?.message) { | ||||||
|           toast.error( |           toast.error( | ||||||
|             `Failed to start export: ${error.response.data.message}`, |             `Failed to start export: ${error.response.data.message}`, | ||||||
|             { position: "top-center" } |             { position: "top-center" }, | ||||||
|           ); |           ); | ||||||
|         } else { |         } else { | ||||||
|           toast.error(`Failed to start export: ${error.message}`, { |           toast.error(`Failed to start export: ${error.message}`, { | ||||||
| @ -148,7 +148,7 @@ function Export() { | |||||||
|         mutate(); |         mutate(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, [deleteClip]); |   }, [deleteClip, mutate]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="w-full h-full overflow-hidden"> |     <div className="w-full h-full overflow-hidden"> | ||||||
| @ -156,7 +156,7 @@ function Export() { | |||||||
| 
 | 
 | ||||||
|       <AlertDialog |       <AlertDialog | ||||||
|         open={deleteClip != undefined} |         open={deleteClip != undefined} | ||||||
|         onOpenChange={(_) => setDeleteClip(undefined)} |         onOpenChange={() => setDeleteClip(undefined)} | ||||||
|       > |       > | ||||||
|         <AlertDialogContent> |         <AlertDialogContent> | ||||||
|           <AlertDialogHeader> |           <AlertDialogHeader> | ||||||
| @ -176,7 +176,7 @@ function Export() { | |||||||
| 
 | 
 | ||||||
|       <Dialog |       <Dialog | ||||||
|         open={selectedClip != undefined} |         open={selectedClip != undefined} | ||||||
|         onOpenChange={(_) => setSelectedClip(undefined)} |         onOpenChange={() => setSelectedClip(undefined)} | ||||||
|       > |       > | ||||||
|         <DialogContent> |         <DialogContent> | ||||||
|           <DialogHeader> |           <DialogHeader> | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ function Live() { | |||||||
| 
 | 
 | ||||||
|   const [layout, setLayout] = usePersistence<"grid" | "list">( |   const [layout, setLayout] = usePersistence<"grid" | "list">( | ||||||
|     "live-layout", |     "live-layout", | ||||||
|     isDesktop ? "grid" : "list" |     isDesktop ? "grid" : "list", | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // recent events
 |   // recent events
 | ||||||
| @ -40,7 +40,7 @@ function Live() { | |||||||
|       updateEvents(); |       updateEvents(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|   }, [eventUpdate]); |   }, [eventUpdate, updateEvents]); | ||||||
| 
 | 
 | ||||||
|   const events = useMemo(() => { |   const events = useMemo(() => { | ||||||
|     if (!allEvents) { |     if (!allEvents) { | ||||||
| @ -76,7 +76,7 @@ function Live() { | |||||||
|     return () => { |     return () => { | ||||||
|       removeEventListener("visibilitychange", visibilityListener); |       removeEventListener("visibilitychange", visibilityListener); | ||||||
|     }; |     }; | ||||||
|   }, []); |   }, [visibilityListener]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="size-full overflow-y-scroll px-2"> |     <div className="size-full overflow-y-scroll px-2"> | ||||||
| @ -123,7 +123,7 @@ function Live() { | |||||||
|       > |       > | ||||||
|         {cameras.map((camera) => { |         {cameras.map((camera) => { | ||||||
|           let grow; |           let grow; | ||||||
|           let aspectRatio = camera.detect.width / camera.detect.height; |           const aspectRatio = camera.detect.width / camera.detect.height; | ||||||
|           if (aspectRatio > 2) { |           if (aspectRatio > 2) { | ||||||
|             grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`; |             grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`; | ||||||
|           } else if (aspectRatio < 1) { |           } else if (aspectRatio < 1) { | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ function Logs() { | |||||||
|         // no op
 |         // no op
 | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [setEndVisible] |     [setEndVisible], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ function Storage() { | |||||||
|       service["storage"]["/media/frigate/recordings"]["total"] != |       service["storage"]["/media/frigate/recordings"]["total"] != | ||||||
|         service["storage"]["/media/frigate/clips"]["total"] |         service["storage"]["/media/frigate/clips"]["total"] | ||||||
|     ); |     ); | ||||||
|   }, service); |   }, [service]); | ||||||
| 
 | 
 | ||||||
|   const getUnitSize = (MB: number) => { |   const getUnitSize = (MB: number) => { | ||||||
|     if (isNaN(MB) || MB < 0) return "Invalid number"; |     if (isNaN(MB) || MB < 0) return "Invalid number"; | ||||||
| @ -106,12 +106,12 @@ function Storage() { | |||||||
|                   </TableCell> |                   </TableCell> | ||||||
|                   <TableCell> |                   <TableCell> | ||||||
|                     {getUnitSize( |                     {getUnitSize( | ||||||
|                       service["storage"]["/media/frigate/recordings"]["used"] |                       service["storage"]["/media/frigate/recordings"]["used"], | ||||||
|                     )} |                     )} | ||||||
|                   </TableCell> |                   </TableCell> | ||||||
|                   <TableCell> |                   <TableCell> | ||||||
|                     {getUnitSize( |                     {getUnitSize( | ||||||
|                       service["storage"]["/media/frigate/recordings"]["total"] |                       service["storage"]["/media/frigate/recordings"]["total"], | ||||||
|                     )} |                     )} | ||||||
|                   </TableCell> |                   </TableCell> | ||||||
|                 </TableRow> |                 </TableRow> | ||||||
| @ -120,12 +120,12 @@ function Storage() { | |||||||
|                     <TableCell>Snapshots</TableCell> |                     <TableCell>Snapshots</TableCell> | ||||||
|                     <TableCell> |                     <TableCell> | ||||||
|                       {getUnitSize( |                       {getUnitSize( | ||||||
|                         service["storage"]["/media/frigate/clips"]["used"] |                         service["storage"]["/media/frigate/clips"]["used"], | ||||||
|                       )} |                       )} | ||||||
|                     </TableCell> |                     </TableCell> | ||||||
|                     <TableCell> |                     <TableCell> | ||||||
|                       {getUnitSize( |                       {getUnitSize( | ||||||
|                         service["storage"]["/media/frigate/clips"]["total"] |                         service["storage"]["/media/frigate/clips"]["total"], | ||||||
|                       )} |                       )} | ||||||
|                     </TableCell> |                     </TableCell> | ||||||
|                   </TableRow> |                   </TableRow> | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ import { useMemo, useRef, useState } from "react"; | |||||||
| import Heading from "@/components/ui/heading"; | import Heading from "@/components/ui/heading"; | ||||||
| import useSWR from "swr"; | import useSWR from "swr"; | ||||||
| import { FrigateConfig } from "@/types/frigateConfig"; | import { FrigateConfig } from "@/types/frigateConfig"; | ||||||
| import { Event } from "@/types/event"; |  | ||||||
| import ActivityIndicator from "@/components/ui/activity-indicator"; | import ActivityIndicator from "@/components/ui/activity-indicator"; | ||||||
| import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; | import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; | ||||||
| import { ReviewData, ReviewSegment, ReviewSeverity } from "@/types/review"; | import { ReviewData, ReviewSegment, ReviewSeverity } from "@/types/review"; | ||||||
| @ -84,19 +83,9 @@ function UIPlayground() { | |||||||
|   const contentRef = useRef<HTMLDivElement>(null); |   const contentRef = useRef<HTMLDivElement>(null); | ||||||
|   const [mockEvents, setMockEvents] = useState<ReviewSegment[]>([]); |   const [mockEvents, setMockEvents] = useState<ReviewSegment[]>([]); | ||||||
|   const [handlebarTime, setHandlebarTime] = useState( |   const [handlebarTime, setHandlebarTime] = useState( | ||||||
|     Math.floor(Date.now() / 1000) - 15 * 60 |     Math.floor(Date.now() / 1000) - 15 * 60, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const recentTimestamp = useMemo(() => { |  | ||||||
|     const now = new Date(); |  | ||||||
|     now.setMinutes(now.getMinutes() - 240); |  | ||||||
|     return now.getTime() / 1000; |  | ||||||
|   }, []); |  | ||||||
|   const { data: events } = useSWR<Event[]>([ |  | ||||||
|     "events", |  | ||||||
|     { limit: 10, after: recentTimestamp }, |  | ||||||
|   ]); |  | ||||||
| 
 |  | ||||||
|   useMemo(() => { |   useMemo(() => { | ||||||
|     const initialEvents = Array.from({ length: 50 }, generateRandomEvent); |     const initialEvents = Array.from({ length: 50 }, generateRandomEvent); | ||||||
|     setMockEvents(initialEvents); |     setMockEvents(initialEvents); | ||||||
| @ -108,16 +97,16 @@ function UIPlayground() { | |||||||
|       return Math.min(...mockEvents.map((event) => event.start_time)); |       return Math.min(...mockEvents.map((event) => event.start_time)); | ||||||
|     } |     } | ||||||
|     return Math.floor(Date.now() / 1000); // Default to current time if no events
 |     return Math.floor(Date.now() / 1000); // Default to current time if no events
 | ||||||
|   }, [events]); |   }, [mockEvents]); | ||||||
| 
 | 
 | ||||||
|   const minimapEndTime = useMemo(() => { |   const minimapEndTime = useMemo(() => { | ||||||
|     if (mockEvents && mockEvents.length > 0) { |     if (mockEvents && mockEvents.length > 0) { | ||||||
|       return Math.max( |       return Math.max( | ||||||
|         ...mockEvents.map((event) => event.end_time ?? event.start_time) |         ...mockEvents.map((event) => event.end_time ?? event.start_time), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return Math.floor(Date.now() / 1000); // Default to current time if no events
 |     return Math.floor(Date.now() / 1000); // Default to current time if no events
 | ||||||
|   }, [events]); |   }, [mockEvents]); | ||||||
| 
 | 
 | ||||||
|   const [zoomLevel, setZoomLevel] = useState(0); |   const [zoomLevel, setZoomLevel] = useState(0); | ||||||
|   const [zoomSettings, setZoomSettings] = useState({ |   const [zoomSettings, setZoomSettings] = useState({ | ||||||
| @ -134,7 +123,7 @@ function UIPlayground() { | |||||||
|   function handleZoomIn() { |   function handleZoomIn() { | ||||||
|     const nextZoomLevel = Math.min( |     const nextZoomLevel = Math.min( | ||||||
|       possibleZoomLevels.length - 1, |       possibleZoomLevels.length - 1, | ||||||
|       zoomLevel + 1 |       zoomLevel + 1, | ||||||
|     ); |     ); | ||||||
|     setZoomLevel(nextZoomLevel); |     setZoomLevel(nextZoomLevel); | ||||||
|     setZoomSettings(possibleZoomLevels[nextZoomLevel]); |     setZoomSettings(possibleZoomLevels[nextZoomLevel]); | ||||||
|  | |||||||
| @ -1,9 +1,4 @@ | |||||||
| import { | import { LuConstruction, LuFileUp, LuFlag, LuVideo } from "react-icons/lu"; | ||||||
|   LuConstruction, |  | ||||||
|   LuFileUp, |  | ||||||
|   LuFlag, |  | ||||||
|   LuVideo, |  | ||||||
| } from "react-icons/lu"; |  | ||||||
| 
 | 
 | ||||||
| export const navbarLinks = [ | export const navbarLinks = [ | ||||||
|   { |   { | ||||||
|  | |||||||
| @ -1 +1,3 @@ | |||||||
| type FilterType = { [searchKey: string]: any }; | // allow any
 | ||||||
|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  | export type FilterType = { [searchKey: string]: any }; | ||||||
|  | |||||||
| @ -199,7 +199,7 @@ export interface CameraConfig { | |||||||
|       coordinates: string; |       coordinates: string; | ||||||
|       filters: Record<string, unknown>; |       filters: Record<string, unknown>; | ||||||
|       inertia: number; |       inertia: number; | ||||||
|       objects: any[]; |       objects: string[]; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| @ -383,7 +383,7 @@ export interface FrigateConfig { | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   telemetry: { |   telemetry: { | ||||||
|     network_interfaces: any[]; |     network_interfaces: string[]; | ||||||
|     stats: { |     stats: { | ||||||
|       amd_gpu_stats: boolean; |       amd_gpu_stats: boolean; | ||||||
|       intel_gpu_stats: boolean; |       intel_gpu_stats: boolean; | ||||||
|  | |||||||
| @ -1 +1 @@ | |||||||
| export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug"; | export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug"; | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| type DynamicPlayback = { | import { Preview } from "./preview"; | ||||||
|  | import { Recording } from "./record"; | ||||||
|  | 
 | ||||||
|  | export type DynamicPlayback = { | ||||||
|   recordings: Recording[]; |   recordings: Recording[]; | ||||||
|   playbackUri: string; |   playbackUri: string; | ||||||
|   preview: Preview | undefined; |   preview: Preview | undefined; | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| type Preview = { | export type Preview = { | ||||||
|     camera: string; |   camera: string; | ||||||
|     src: string; |   src: string; | ||||||
|     type: string; |   type: string; | ||||||
|     start: number; |   start: number; | ||||||
|     end: number; |   end: number; | ||||||
|   }; | }; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| type Recording = { | export type Recording = { | ||||||
|   id: string; |   id: string; | ||||||
|   camera: string; |   camera: string; | ||||||
|   start_time: number; |   start_time: number; | ||||||
| @ -11,7 +11,7 @@ type Recording = { | |||||||
|   dBFS: number; |   dBFS: number; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type RecordingSegment = { | export type RecordingSegment = { | ||||||
|   id: string; |   id: string; | ||||||
|   start_time: number; |   start_time: number; | ||||||
|   end_time: number; |   end_time: number; | ||||||
| @ -21,7 +21,7 @@ type RecordingSegment = { | |||||||
|   duration: number; |   duration: number; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type RecordingActivity = { | export type RecordingActivity = { | ||||||
|   [hour: number]: RecordingSegmentActivity[]; |   [hour: number]: RecordingSegmentActivity[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,60 +1,60 @@ | |||||||
| export interface FrigateStats { | export interface FrigateStats { | ||||||
|     cameras: { [camera_name: string]: CameraStats }; |   cameras: { [camera_name: string]: CameraStats }; | ||||||
|     cpu_usages: { [pid: string]: CpuStats }; |   cpu_usages: { [pid: string]: CpuStats }; | ||||||
|     detectors: { [detectorKey: string]: DetectorStats }; |   detectors: { [detectorKey: string]: DetectorStats }; | ||||||
|     gpu_usages?: { [gpuKey: string]: GpuStats }; |   gpu_usages?: { [gpuKey: string]: GpuStats }; | ||||||
|     processes: { [processKey: string]: ExtraProcessStats }; |   processes: { [processKey: string]: ExtraProcessStats }; | ||||||
|     service: ServiceStats; |   service: ServiceStats; | ||||||
|     detection_fps: number; |   detection_fps: number; | ||||||
|   } | } | ||||||
| 
 | 
 | ||||||
|   export type CameraStats = { | export type CameraStats = { | ||||||
|     audio_dBFPS: number; |   audio_dBFPS: number; | ||||||
|     audio_rms: number; |   audio_rms: number; | ||||||
|     camera_fps: number; |   camera_fps: number; | ||||||
|     capture_pid: number; |   capture_pid: number; | ||||||
|     detection_enabled: number; |   detection_enabled: number; | ||||||
|     detection_fps: number; |   detection_fps: number; | ||||||
|     ffmpeg_pid: number; |   ffmpeg_pid: number; | ||||||
|     pid: number; |   pid: number; | ||||||
|     process_fps: number; |   process_fps: number; | ||||||
|     skipped_fps: number; |   skipped_fps: number; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type CpuStats = { | export type CpuStats = { | ||||||
|     cmdline: string; |   cmdline: string; | ||||||
|     cpu: string; |   cpu: string; | ||||||
|     cpu_average: string; |   cpu_average: string; | ||||||
|     mem: string; |   mem: string; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type DetectorStats = { | export type DetectorStats = { | ||||||
|     detection_start: number; |   detection_start: number; | ||||||
|     inference_speed: number; |   inference_speed: number; | ||||||
|     pid: number; |   pid: number; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type ExtraProcessStats = { | export type ExtraProcessStats = { | ||||||
|     pid: number; |   pid: number; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type GpuStats = { | export type GpuStats = { | ||||||
|     gpu: string; |   gpu: string; | ||||||
|     mem: string; |   mem: string; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type ServiceStats = { | export type ServiceStats = { | ||||||
|     last_updated: number; |   last_updated: number; | ||||||
|     storage: { [path: string]: StorageStats }; |   storage: { [path: string]: StorageStats }; | ||||||
|     temperatures: { [apex: string]: number }; |   temperatures: { [apex: string]: number }; | ||||||
|     update: number; |   update: number; | ||||||
|     latest_version: string; |   latest_version: string; | ||||||
|     version: string; |   version: string; | ||||||
|   }; | }; | ||||||
| 
 | 
 | ||||||
|   export type StorageStats = { | export type StorageStats = { | ||||||
|     free: number; |   free: number; | ||||||
|     total: number; |   total: number; | ||||||
|     used: number; |   used: number; | ||||||
|     mount_type: string; |   mount_type: string; | ||||||
|   }; | }; | ||||||
|  | |||||||
| @ -1,31 +1,33 @@ | |||||||
| type Timeline = { | type Timeline = { | ||||||
|  |   camera: string; | ||||||
|  |   timestamp: number; | ||||||
|  |   data: { | ||||||
|     camera: string; |     camera: string; | ||||||
|     timestamp: number; |     label: string; | ||||||
|     data: { |     sub_label: string; | ||||||
|       camera: string; |     box?: [number, number, number, number]; | ||||||
|       label: string; |     region: [number, number, number, number]; | ||||||
|       sub_label: string; |     attribute: string; | ||||||
|       box?: [number, number, number, number]; |     zones: string[]; | ||||||
|       region: [number, number, number, number]; |  | ||||||
|       attribute: string; |  | ||||||
|       zones: string[]; |  | ||||||
|     }; |  | ||||||
|     class_type: |  | ||||||
|       | "visible" |  | ||||||
|       | "gone" |  | ||||||
|       | "entered_zone" |  | ||||||
|       | "attribute" |  | ||||||
|       | "active" |  | ||||||
|       | "stationary" |  | ||||||
|       | "heard" |  | ||||||
|       | "external"; |  | ||||||
|     source_id: string; |  | ||||||
|     source: string; |  | ||||||
|   }; |   }; | ||||||
|  |   class_type: | ||||||
|  |     | "visible" | ||||||
|  |     | "gone" | ||||||
|  |     | "entered_zone" | ||||||
|  |     | "attribute" | ||||||
|  |     | "active" | ||||||
|  |     | "stationary" | ||||||
|  |     | "heard" | ||||||
|  |     | "external"; | ||||||
|  |   source_id: string; | ||||||
|  |   source: string; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
|   type HourlyTimeline = { | // may be used in the future, keep for now for reference
 | ||||||
|     start: number; | // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|     end: number; | type HourlyTimeline = { | ||||||
|     count: number; |   start: number; | ||||||
|     hours: { [key: string]: Timeline[] }; |   end: number; | ||||||
|   }; |   count: number; | ||||||
|  |   hours: { [key: string]: Timeline[] }; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -40,4 +40,4 @@ export interface FrigateEvent { | |||||||
|   after: FrigateObjectState; |   after: FrigateObjectState; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type ToggleableSetting = "ON" | "OFF" | export type ToggleableSetting = "ON" | "OFF"; | ||||||
|  | |||||||
| @ -132,7 +132,7 @@ export const formatUnixTimestampToDateTime = ( | |||||||
|     date_style?: "full" | "long" | "medium" | "short"; |     date_style?: "full" | "long" | "medium" | "short"; | ||||||
|     time_style?: "full" | "long" | "medium" | "short"; |     time_style?: "full" | "long" | "medium" | "short"; | ||||||
|     strftime_fmt?: string; |     strftime_fmt?: string; | ||||||
|   } |   }, | ||||||
| ): string => { | ): string => { | ||||||
|   const { timezone, time_format, date_style, time_style, strftime_fmt } = |   const { timezone, time_format, date_style, time_style, strftime_fmt } = | ||||||
|     config; |     config; | ||||||
| @ -187,7 +187,7 @@ export const formatUnixTimestampToDateTime = ( | |||||||
| 
 | 
 | ||||||
|       return `${date.toLocaleDateString( |       return `${date.toLocaleDateString( | ||||||
|         locale, |         locale, | ||||||
|         dateOptions |         dateOptions, | ||||||
|       )} ${date.toLocaleTimeString(locale, timeOptions)}`;
 |       )} ${date.toLocaleTimeString(locale, timeOptions)}`;
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -213,7 +213,7 @@ interface DurationToken { | |||||||
|  */ |  */ | ||||||
| export const getDurationFromTimestamps = ( | export const getDurationFromTimestamps = ( | ||||||
|   start_time: number, |   start_time: number, | ||||||
|   end_time: number | null |   end_time: number | null, | ||||||
| ): string => { | ): string => { | ||||||
|   if (isNaN(start_time)) { |   if (isNaN(start_time)) { | ||||||
|     return "Invalid start time"; |     return "Invalid start time"; | ||||||
| @ -259,7 +259,7 @@ const getUTCOffset = (date: Date, timezone: string): number => { | |||||||
| 
 | 
 | ||||||
|   // Otherwise, calculate offset using provided timezone
 |   // Otherwise, calculate offset using provided timezone
 | ||||||
|   const utcDate = new Date( |   const utcDate = new Date( | ||||||
|     date.getTime() - date.getTimezoneOffset() * 60 * 1000 |     date.getTime() - date.getTimezoneOffset() * 60 * 1000, | ||||||
|   ); |   ); | ||||||
|   // locale of en-CA is required for proper locale format
 |   // locale of en-CA is required for proper locale format
 | ||||||
|   let iso = utcDate |   let iso = utcDate | ||||||
|  | |||||||
| @ -102,7 +102,7 @@ export function getTimelineItemDescription(timelineItem: Timeline) { | |||||||
|       ) { |       ) { | ||||||
|         title = `${timelineItem.data.attribute.replaceAll( |         title = `${timelineItem.data.attribute.replaceAll( | ||||||
|           "_", |           "_", | ||||||
|           " " |           " ", | ||||||
|         )} detected for ${label}`;
 |         )} detected for ${label}`;
 | ||||||
|       } else { |       } else { | ||||||
|         title = `${ |         title = `${ | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import DynamicVideoPlayer, { | |||||||
| } from "@/components/player/DynamicVideoPlayer"; | } from "@/components/player/DynamicVideoPlayer"; | ||||||
| import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; | import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; | ||||||
| import { Button } from "@/components/ui/button"; | import { Button } from "@/components/ui/button"; | ||||||
|  | import { Preview } from "@/types/preview"; | ||||||
| import { ReviewSegment } from "@/types/review"; | import { ReviewSegment } from "@/types/review"; | ||||||
| import { getChunkedTimeRange } from "@/utils/timelineUtil"; | import { getChunkedTimeRange } from "@/utils/timelineUtil"; | ||||||
| import { useEffect, useMemo, useRef, useState } from "react"; | import { useEffect, useMemo, useRef, useState } from "react"; | ||||||
| @ -31,7 +32,7 @@ export default function DesktopRecordingView({ | |||||||
| 
 | 
 | ||||||
|   const timeRange = useMemo( |   const timeRange = useMemo( | ||||||
|     () => getChunkedTimeRange(selectedReview.start_time), |     () => getChunkedTimeRange(selectedReview.start_time), | ||||||
|     [] |     [selectedReview], | ||||||
|   ); |   ); | ||||||
|   const [selectedRangeIdx, setSelectedRangeIdx] = useState( |   const [selectedRangeIdx, setSelectedRangeIdx] = useState( | ||||||
|     timeRange.ranges.findIndex((chunk) => { |     timeRange.ranges.findIndex((chunk) => { | ||||||
| @ -39,7 +40,7 @@ export default function DesktopRecordingView({ | |||||||
|         chunk.start <= selectedReview.start_time && |         chunk.start <= selectedReview.start_time && | ||||||
|         chunk.end >= selectedReview.start_time |         chunk.end >= selectedReview.start_time | ||||||
|       ); |       ); | ||||||
|     }) |     }), | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // move to next clip
 |   // move to next clip
 | ||||||
| @ -55,13 +56,13 @@ export default function DesktopRecordingView({ | |||||||
|         setSelectedRangeIdx(selectedRangeIdx - 1); |         setSelectedRangeIdx(selectedRangeIdx - 1); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, [playerReady, selectedRangeIdx]); |   }, [playerReady, selectedRangeIdx, timeRange]); | ||||||
| 
 | 
 | ||||||
|   // scrubbing and timeline state
 |   // scrubbing and timeline state
 | ||||||
| 
 | 
 | ||||||
|   const [scrubbing, setScrubbing] = useState(false); |   const [scrubbing, setScrubbing] = useState(false); | ||||||
|   const [currentTime, setCurrentTime] = useState<number>( |   const [currentTime, setCurrentTime] = useState<number>( | ||||||
|     selectedReview?.start_time || Date.now() / 1000 |     selectedReview?.start_time || Date.now() / 1000, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -74,6 +75,9 @@ export default function DesktopRecordingView({ | |||||||
|     if (!scrubbing) { |     if (!scrubbing) { | ||||||
|       controllerRef.current?.seekToTimestamp(currentTime, true); |       controllerRef.current?.seekToTimestamp(currentTime, true); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // we only want to seek when user stops scrubbing
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [scrubbing]); |   }, [scrubbing]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -100,7 +104,7 @@ export default function DesktopRecordingView({ | |||||||
| 
 | 
 | ||||||
|             controllerRef.current?.seekToTimestamp( |             controllerRef.current?.seekToTimestamp( | ||||||
|               selectedReview.start_time, |               selectedReview.start_time, | ||||||
|               true |               true, | ||||||
|             ); |             ); | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import ActivityIndicator from "@/components/ui/activity-indicator"; | |||||||
| import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; | ||||||
| import { useEventUtils } from "@/hooks/use-event-utils"; | import { useEventUtils } from "@/hooks/use-event-utils"; | ||||||
| import { FrigateConfig } from "@/types/frigateConfig"; | import { FrigateConfig } from "@/types/frigateConfig"; | ||||||
|  | import { Preview } from "@/types/preview"; | ||||||
| import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; | import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; | ||||||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||||||
| import { isDesktop, isMobile } from "react-device-detect"; | import { isDesktop, isMobile } from "react-device-detect"; | ||||||
| @ -84,7 +85,7 @@ export default function EventView({ | |||||||
| 
 | 
 | ||||||
|   const { alignStartDateToTimeline } = useEventUtils( |   const { alignStartDateToTimeline } = useEventUtils( | ||||||
|     reviewItems.all, |     reviewItems.all, | ||||||
|     segmentDuration |     segmentDuration, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const currentItems = useMemo(() => { |   const currentItems = useMemo(() => { | ||||||
| @ -103,6 +104,8 @@ export default function EventView({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return contentRef.current.scrollHeight > contentRef.current.clientHeight; |     return contentRef.current.scrollHeight > contentRef.current.clientHeight; | ||||||
|  |     // we know that these deps are correct
 | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|   }, [contentRef.current?.scrollHeight, severity]); |   }, [contentRef.current?.scrollHeight, severity]); | ||||||
| 
 | 
 | ||||||
|   // review interaction
 |   // review interaction
 | ||||||
| @ -123,7 +126,7 @@ export default function EventView({ | |||||||
|         // no op
 |         // no op
 | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [isValidating, reachedEnd] |     [isValidating, reachedEnd, loadNextPage], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const [minimap, setMinimap] = useState<string[]>([]); |   const [minimap, setMinimap] = useState<string[]>([]); | ||||||
| @ -148,7 +151,7 @@ export default function EventView({ | |||||||
|           setMinimap([...visibleTimestamps]); |           setMinimap([...visibleTimestamps]); | ||||||
|         }); |         }); | ||||||
|       }, |       }, | ||||||
|       { root: contentRef.current, threshold: isDesktop ? 0.1 : 0.5 } |       { root: contentRef.current, threshold: isDesktop ? 0.1 : 0.5 }, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     return () => { |     return () => { | ||||||
| @ -167,7 +170,7 @@ export default function EventView({ | |||||||
|         // no op
 |         // no op
 | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     [minimapObserver] |     [minimapObserver], | ||||||
|   ); |   ); | ||||||
|   const minimapBounds = useMemo(() => { |   const minimapBounds = useMemo(() => { | ||||||
|     const data = { |     const data = { | ||||||
| @ -177,7 +180,7 @@ export default function EventView({ | |||||||
|     const list = minimap.sort(); |     const list = minimap.sort(); | ||||||
| 
 | 
 | ||||||
|     if (list.length > 0) { |     if (list.length > 0) { | ||||||
|       data.end = parseFloat(list.at(-1)!!); |       data.end = parseFloat(list.at(-1) || "0"); | ||||||
|       data.start = parseFloat(list[0]); |       data.start = parseFloat(list[0]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -260,12 +263,12 @@ export default function EventView({ | |||||||
|               currentItems.map((value, segIdx) => { |               currentItems.map((value, segIdx) => { | ||||||
|                 const lastRow = segIdx == reviewItems[severity].length - 1; |                 const lastRow = segIdx == reviewItems[severity].length - 1; | ||||||
|                 const relevantPreview = Object.values( |                 const relevantPreview = Object.values( | ||||||
|                   relevantPreviews || [] |                   relevantPreviews || [], | ||||||
|                 ).find( |                 ).find( | ||||||
|                   (preview) => |                   (preview) => | ||||||
|                     preview.camera == value.camera && |                     preview.camera == value.camera && | ||||||
|                     preview.start < value.start_time && |                     preview.start < value.start_time && | ||||||
|                     preview.end > value.end_time |                     preview.end > value.end_time, | ||||||
|                 ); |                 ); | ||||||
| 
 | 
 | ||||||
|                 return ( |                 return ( | ||||||
|  | |||||||
| @ -1,45 +1,45 @@ | |||||||
| /// <reference types="vitest" />
 | /// <reference types="vitest" />
 | ||||||
| import path from "path" | import path from "path"; | ||||||
| import { defineConfig } from 'vite' | import { defineConfig } from "vite"; | ||||||
| import react from '@vitejs/plugin-react-swc' | import react from "@vitejs/plugin-react-swc"; | ||||||
| import monacoEditorPlugin from 'vite-plugin-monaco-editor'; | import monacoEditorPlugin from "vite-plugin-monaco-editor"; | ||||||
| 
 | 
 | ||||||
| // https://vitejs.dev/config/
 | // https://vitejs.dev/config/
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   define: { |   define: { | ||||||
|     'import.meta.vitest': 'undefined', |     "import.meta.vitest": "undefined", | ||||||
|   }, |   }, | ||||||
|   server: { |   server: { | ||||||
|     proxy: { |     proxy: { | ||||||
|       '/api': { |       "/api": { | ||||||
|         target: 'http://localhost:5000', |         target: "http://localhost:5000", | ||||||
|         ws: true, |         ws: true, | ||||||
|       }, |       }, | ||||||
|       '/vod': { |       "/vod": { | ||||||
|         target: 'http://localhost:5000' |         target: "http://localhost:5000", | ||||||
|       }, |       }, | ||||||
|       '/clips': { |       "/clips": { | ||||||
|         target: 'http://localhost:5000' |         target: "http://localhost:5000", | ||||||
|       }, |       }, | ||||||
|       '/exports': { |       "/exports": { | ||||||
|         target: 'http://localhost:5000' |         target: "http://localhost:5000", | ||||||
|       }, |       }, | ||||||
|       '/ws': { |       "/ws": { | ||||||
|         target: 'ws://localhost:5000', |         target: "ws://localhost:5000", | ||||||
|         ws: true, |         ws: true, | ||||||
|       }, |       }, | ||||||
|       '/live': { |       "/live": { | ||||||
|         target: 'ws://localhost:5000', |         target: "ws://localhost:5000", | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|         ws: true, |         ws: true, | ||||||
|       }, |       }, | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
|   plugins: [ |   plugins: [ | ||||||
|     react(), |     react(), | ||||||
|     monacoEditorPlugin.default({ |     monacoEditorPlugin.default({ | ||||||
|       customWorkers: [{ label: 'yaml', entry: 'monaco-yaml/yaml.worker' }], |       customWorkers: [{ label: "yaml", entry: "monaco-yaml/yaml.worker" }], | ||||||
|       languageWorkers: ['editorWorkerService'], // we don't use any of the default languages
 |       languageWorkers: ["editorWorkerService"], // we don't use any of the default languages
 | ||||||
|     }), |     }), | ||||||
|   ], |   ], | ||||||
|   resolve: { |   resolve: { | ||||||
| @ -48,17 +48,20 @@ export default defineConfig({ | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   test: { |   test: { | ||||||
|     environment: 'jsdom', |     environment: "jsdom", | ||||||
|     alias: { |     alias: { | ||||||
|       'testing-library': path.resolve(__dirname, './__test__/testing-library.js'), |       "testing-library": path.resolve( | ||||||
|  |         __dirname, | ||||||
|  |         "./__test__/testing-library.js", | ||||||
|  |       ), | ||||||
|     }, |     }, | ||||||
|     setupFiles: ['./__test__/test-setup.ts'], |     setupFiles: ["./__test__/test-setup.ts"], | ||||||
|     includeSource: ['src/**/*.{js,jsx,ts,tsx}'], |     includeSource: ["src/**/*.{js,jsx,ts,tsx}"], | ||||||
|     coverage: { |     coverage: { | ||||||
|       reporter: ['text-summary', 'text'], |       reporter: ["text-summary", "text"], | ||||||
|     }, |     }, | ||||||
|     mockReset: true, |     mockReset: true, | ||||||
|     restoreMocks: true, |     restoreMocks: true, | ||||||
|     globals: true, |     globals: true, | ||||||
|   }, |   }, | ||||||
| }) | }); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user