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