Fix linter and fix lint issues (#10141)

This commit is contained in:
Nicolas Mowen 2024-02-28 15:23:56 -07:00 committed by GitHub
parent b6ef1e4330
commit 3bf2a496e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 527 additions and 418 deletions

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -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 || "/"}`;

View File

@ -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 };
} }

View File

@ -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,
}); });

View File

@ -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 (

View File

@ -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({

View File

@ -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);

View File

@ -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(() => {

View File

@ -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}>

View File

@ -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>;

View File

@ -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" />

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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;
} }

View File

@ -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 />;

View File

@ -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 (

View File

@ -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]);

View File

@ -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 (

View File

@ -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");

View File

@ -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,

View File

@ -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 (

View File

@ -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;
} }

View File

@ -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(() => {

View File

@ -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;
}, },
]; ];

View File

@ -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,

View File

@ -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 (

View File

@ -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 {

View File

@ -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 };

View File

@ -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]);
} }

View File

@ -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];

View File

@ -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(() => {

View File

@ -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,
}; };
}; };

View File

@ -1,8 +1,8 @@
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" },
@ -10,16 +10,16 @@ const formatter = new Intl.RelativeTimeFormat(undefined, {
{ 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;
} }
}

View File

@ -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));
} }

View File

@ -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>,
) );

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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) {

View File

@ -57,7 +57,7 @@ function Logs() {
// no op // no op
} }
}, },
[setEndVisible] [setEndVisible],
); );
return ( return (

View File

@ -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>

View File

@ -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]);

View File

@ -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 = [
{ {

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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;
}; };

View File

@ -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[];
}; };

View File

@ -6,9 +6,9 @@ export interface FrigateStats {
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;
@ -19,42 +19,42 @@ export interface FrigateStats {
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;
}; };

View File

@ -21,11 +21,13 @@ type Timeline = {
| "external"; | "external";
source_id: string; source_id: string;
source: string; source: string;
}; };
type HourlyTimeline = { // may be used in the future, keep for now for reference
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type HourlyTimeline = {
start: number; start: number;
end: number; end: number;
count: number; count: number;
hours: { [key: string]: Timeline[] }; hours: { [key: string]: Timeline[] };
}; };

View File

@ -40,4 +40,4 @@ export interface FrigateEvent {
after: FrigateObjectState; after: FrigateObjectState;
} }
export type ToggleableSetting = "ON" | "OFF" export type ToggleableSetting = "ON" | "OFF";

View File

@ -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

View File

@ -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 = `${

View File

@ -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,
); );
}} }}
/> />

View File

@ -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 (

View File

@ -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,
}, },
}) });