mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-12 19:37:35 +03:00
Compare commits
1 Commits
856ee99f90
...
5c3c0be94d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c3c0be94d |
4461
web/package-lock.json
generated
4461
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"postinstall": "patch-package",
|
||||
"build": "tsc && vite build --base=/BASE_PATH/",
|
||||
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .",
|
||||
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
|
||||
@ -33,7 +32,7 @@
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
@ -51,7 +50,8 @@
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"framer-motion": "^12.35.0",
|
||||
"embla-carousel-react": "^8.2.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"hls.js": "^1.5.20",
|
||||
"i18next": "^24.2.0",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
@ -61,28 +61,30 @@
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.477.0",
|
||||
"monaco-yaml": "^5.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-themes": "^0.3.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^19.2.4",
|
||||
"react": "^18.3.1",
|
||||
"react-apexcharts": "^1.4.1",
|
||||
"react-day-picker": "^9.7.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
"react-grid-layout": "^1.5.0",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-konva": "^19.2.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-tracked": "^2.0.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-use-websocket": "^4.8.1",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"recoil": "^0.7.7",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
"sonner": "^1.5.0",
|
||||
"sort-by": "^1.2.0",
|
||||
"strftime": "^0.10.3",
|
||||
"swr": "^2.3.2",
|
||||
@ -90,7 +92,7 @@
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-long-press": "^3.2.0",
|
||||
"vaul": "^1.1.2",
|
||||
"vaul": "^0.9.1",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@ -99,8 +101,11 @@
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-grid-layout": "^1.3.5",
|
||||
"@types/react-icons": "^3.0.0",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"@types/strftime": "^0.9.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
@ -111,26 +116,19 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.2.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.8",
|
||||
"eslint-plugin-vitest-globals": "^1.5.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^24.1.1",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"msw": "^2.3.5",
|
||||
"patch-package": "^8.0.1",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.4.1",
|
||||
"vitest": "^3.0.7"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-slot": "1.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.js b/node_modules/@radix-ui/react-compose-refs/dist/index.js
|
||||
index 5ba7a95..65aa7be 100644
|
||||
--- a/node_modules/@radix-ui/react-compose-refs/dist/index.js
|
||||
+++ b/node_modules/@radix-ui/react-compose-refs/dist/index.js
|
||||
@@ -69,6 +69,31 @@ function composeRefs(...refs) {
|
||||
};
|
||||
}
|
||||
function useComposedRefs(...refs) {
|
||||
- return React.useCallback(composeRefs(...refs), refs);
|
||||
+ const refsRef = React.useRef(refs);
|
||||
+ React.useLayoutEffect(() => {
|
||||
+ refsRef.current = refs;
|
||||
+ });
|
||||
+ return React.useCallback((node) => {
|
||||
+ let hasCleanup = false;
|
||||
+ const cleanups = refsRef.current.map((ref) => {
|
||||
+ const cleanup = setRef(ref, node);
|
||||
+ if (!hasCleanup && typeof cleanup === "function") {
|
||||
+ hasCleanup = true;
|
||||
+ }
|
||||
+ return cleanup;
|
||||
+ });
|
||||
+ if (hasCleanup) {
|
||||
+ return () => {
|
||||
+ for (let i = 0; i < cleanups.length; i++) {
|
||||
+ const cleanup = cleanups[i];
|
||||
+ if (typeof cleanup === "function") {
|
||||
+ cleanup();
|
||||
+ } else {
|
||||
+ setRef(refsRef.current[i], null);
|
||||
+ }
|
||||
+ }
|
||||
+ };
|
||||
+ }
|
||||
+ }, []);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
diff --git a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
|
||||
index 7dd9172..d1b53a5 100644
|
||||
--- a/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
|
||||
+++ b/node_modules/@radix-ui/react-compose-refs/dist/index.mjs
|
||||
@@ -32,7 +32,32 @@ function composeRefs(...refs) {
|
||||
};
|
||||
}
|
||||
function useComposedRefs(...refs) {
|
||||
- return React.useCallback(composeRefs(...refs), refs);
|
||||
+ const refsRef = React.useRef(refs);
|
||||
+ React.useLayoutEffect(() => {
|
||||
+ refsRef.current = refs;
|
||||
+ });
|
||||
+ return React.useCallback((node) => {
|
||||
+ let hasCleanup = false;
|
||||
+ const cleanups = refsRef.current.map((ref) => {
|
||||
+ const cleanup = setRef(ref, node);
|
||||
+ if (!hasCleanup && typeof cleanup === "function") {
|
||||
+ hasCleanup = true;
|
||||
+ }
|
||||
+ return cleanup;
|
||||
+ });
|
||||
+ if (hasCleanup) {
|
||||
+ return () => {
|
||||
+ for (let i = 0; i < cleanups.length; i++) {
|
||||
+ const cleanup = cleanups[i];
|
||||
+ if (typeof cleanup === "function") {
|
||||
+ cleanup();
|
||||
+ } else {
|
||||
+ setRef(refsRef.current[i], null);
|
||||
+ }
|
||||
+ }
|
||||
+ };
|
||||
+ }
|
||||
+ }, []);
|
||||
}
|
||||
export {
|
||||
composeRefs,
|
||||
@ -1,46 +0,0 @@
|
||||
diff --git a/node_modules/@radix-ui/react-slot/dist/index.js b/node_modules/@radix-ui/react-slot/dist/index.js
|
||||
index 3691205..3b62ea8 100644
|
||||
--- a/node_modules/@radix-ui/react-slot/dist/index.js
|
||||
+++ b/node_modules/@radix-ui/react-slot/dist/index.js
|
||||
@@ -85,11 +85,12 @@ function createSlotClone(ownerName) {
|
||||
if (isLazyComponent(children) && typeof use === "function") {
|
||||
children = use(children._payload);
|
||||
}
|
||||
+ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null;
|
||||
+ const composedRef = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, childrenRef);
|
||||
if (React.isValidElement(children)) {
|
||||
- const childrenRef = getElementRef(children);
|
||||
const props2 = mergeProps(slotProps, children.props);
|
||||
if (children.type !== React.Fragment) {
|
||||
- props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef;
|
||||
+ props2.ref = forwardedRef ? composedRef : childrenRef;
|
||||
}
|
||||
return React.cloneElement(children, props2);
|
||||
}
|
||||
diff --git a/node_modules/@radix-ui/react-slot/dist/index.mjs b/node_modules/@radix-ui/react-slot/dist/index.mjs
|
||||
index d7ea374..a990150 100644
|
||||
--- a/node_modules/@radix-ui/react-slot/dist/index.mjs
|
||||
+++ b/node_modules/@radix-ui/react-slot/dist/index.mjs
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/slot.tsx
|
||||
import * as React from "react";
|
||||
-import { composeRefs } from "@radix-ui/react-compose-refs";
|
||||
+import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs";
|
||||
import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
|
||||
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
|
||||
var use = React[" use ".trim().toString()];
|
||||
@@ -45,11 +45,12 @@ function createSlotClone(ownerName) {
|
||||
if (isLazyComponent(children) && typeof use === "function") {
|
||||
children = use(children._payload);
|
||||
}
|
||||
+ const childrenRef = React.isValidElement(children) ? getElementRef(children) : null;
|
||||
+ const composedRef = useComposedRefs(forwardedRef, childrenRef);
|
||||
if (React.isValidElement(children)) {
|
||||
- const childrenRef = getElementRef(children);
|
||||
const props2 = mergeProps(slotProps, children.props);
|
||||
if (children.type !== React.Fragment) {
|
||||
- props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
|
||||
+ props2.ref = forwardedRef ? composedRef : childrenRef;
|
||||
}
|
||||
return React.cloneElement(children, props2);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
diff --git a/node_modules/react-use-websocket/dist/lib/use-websocket.js b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||
index f01db48..b30aff2 100644
|
||||
--- a/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||
+++ b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||
@@ -139,15 +139,15 @@ var useWebSocket = function (url, options, connect) {
|
||||
}
|
||||
protectedSetLastMessage = function (message) {
|
||||
if (!expectClose_1) {
|
||||
- (0, react_dom_1.flushSync)(function () { return setLastMessage(message); });
|
||||
+ setLastMessage(message);
|
||||
}
|
||||
};
|
||||
protectedSetReadyState = function (state) {
|
||||
if (!expectClose_1) {
|
||||
- (0, react_dom_1.flushSync)(function () { return setReadyState(function (prev) {
|
||||
+ setReadyState(function (prev) {
|
||||
var _a;
|
||||
return (__assign(__assign({}, prev), (convertedUrl.current && (_a = {}, _a[convertedUrl.current] = state, _a))));
|
||||
- }); });
|
||||
+ });
|
||||
}
|
||||
};
|
||||
if (createOrJoin_1) {
|
||||
@ -114,17 +114,10 @@ interface PropertyElement {
|
||||
content: React.ReactElement;
|
||||
}
|
||||
|
||||
/** Shape of the props that RJSF injects into each property element. */
|
||||
interface RjsfElementProps {
|
||||
schema?: { type?: string | string[] };
|
||||
uiSchema?: Record<string, unknown> & {
|
||||
"ui:widget"?: string;
|
||||
"ui:options"?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function isObjectLikeElement(item: PropertyElement) {
|
||||
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
|
||||
const fieldSchema = item.content.props?.schema as
|
||||
| { type?: string | string[] }
|
||||
| undefined;
|
||||
return fieldSchema?.type === "object";
|
||||
}
|
||||
|
||||
@ -170,21 +163,16 @@ function GridLayoutObjectFieldTemplate(
|
||||
|
||||
// Override the properties rendering with grid layout
|
||||
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
||||
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
|
||||
"hidden";
|
||||
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
|
||||
|
||||
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
||||
|
||||
// Separate regular and advanced properties
|
||||
const advancedProps = visibleProps.filter(
|
||||
(p) =>
|
||||
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||
?.advanced === true,
|
||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
||||
);
|
||||
const regularProps = visibleProps.filter(
|
||||
(p) =>
|
||||
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||
?.advanced !== true,
|
||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
||||
);
|
||||
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
||||
isPathModified([...fieldPath, prop.name]),
|
||||
|
||||
@ -448,9 +448,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const errorsProps = errors?.props as { errors?: unknown[] } | undefined;
|
||||
const hasFieldErrors = !!errors && (errorsProps?.errors?.length ?? 0) > 0;
|
||||
|
||||
const renderStandardLabel = () => {
|
||||
if (!shouldRenderStandardLabel) {
|
||||
return null;
|
||||
@ -462,7 +459,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
isModified && "text-danger",
|
||||
hasFieldErrors && "text-destructive",
|
||||
errors && errors.props?.errors?.length > 0 && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
@ -500,7 +497,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
isModified && "text-danger",
|
||||
hasFieldErrors && "text-destructive",
|
||||
errors && errors.props?.errors?.length > 0 && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
// Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields
|
||||
// Renders simple nullable types as single inputs instead of dropdowns
|
||||
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
MultiSchemaFieldTemplateProps,
|
||||
StrictRJSFSchema,
|
||||
|
||||
@ -25,15 +25,6 @@ import {
|
||||
import get from "lodash/get";
|
||||
import { AddPropertyButton, AdvancedCollapsible } from "../components";
|
||||
|
||||
/** Shape of the props that RJSF injects into each property element. */
|
||||
interface RjsfElementProps {
|
||||
schema?: { type?: string | string[] };
|
||||
uiSchema?: Record<string, unknown> & {
|
||||
"ui:widget"?: string;
|
||||
"ui:options"?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const {
|
||||
title,
|
||||
@ -191,21 +182,16 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
uiSchema?.["ui:options"]?.disableNestedCard === true;
|
||||
|
||||
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
||||
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
|
||||
"hidden";
|
||||
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
|
||||
|
||||
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
||||
|
||||
// Check for advanced section grouping
|
||||
const advancedProps = visibleProps.filter(
|
||||
(p) =>
|
||||
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||
?.advanced === true,
|
||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
||||
);
|
||||
const regularProps = visibleProps.filter(
|
||||
(p) =>
|
||||
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||
?.advanced !== true,
|
||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
||||
);
|
||||
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
||||
checkSubtreeModified([...fieldPath, prop.name]),
|
||||
@ -347,7 +333,9 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
|
||||
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
||||
const isObjectLikeField = (item: (typeof properties)[number]) => {
|
||||
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
|
||||
const fieldSchema = item.content.props.schema as
|
||||
| { type?: string | string[] }
|
||||
| undefined;
|
||||
return fieldSchema?.type === "object";
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { t } from "i18next";
|
||||
import type { JSX } from "react";
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
||||
|
||||
interface IProp {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LogSeverity } from "@/types/log";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { ReactNode, useMemo, useRef } from "react";
|
||||
import { isIOS } from "react-device-detect";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
|
||||
type ChipProps = {
|
||||
className?: string;
|
||||
@ -17,31 +17,39 @@ export default function Chip({
|
||||
in: inProp = true,
|
||||
onClick,
|
||||
}: ChipProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{inProp && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
className={cn(
|
||||
"flex items-center rounded-2xl px-2 py-1.5",
|
||||
className,
|
||||
!isIOS && "z-10",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
return (
|
||||
<CSSTransition
|
||||
in={inProp}
|
||||
nodeRef={nodeRef}
|
||||
timeout={500}
|
||||
classNames={{
|
||||
enter: "opacity-0",
|
||||
enterActive: "opacity-100 transition-opacity duration-500 ease-in-out",
|
||||
exit: "opacity-100",
|
||||
exitActive: "opacity-0 transition-opacity duration-500 ease-in-out",
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<div
|
||||
ref={nodeRef}
|
||||
className={cn(
|
||||
"flex items-center rounded-2xl px-2 py-1.5",
|
||||
className,
|
||||
!isIOS && "z-10",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -55,9 +55,9 @@ export function MobilePage({
|
||||
});
|
||||
|
||||
return (
|
||||
<MobilePageContext value={{ open, onOpenChange: setOpen }}>
|
||||
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
||||
{children}
|
||||
</MobilePageContext>
|
||||
</MobilePageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ export function MobilePagePortal({
|
||||
type MobilePageContentProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollerRef?: React.RefObject<HTMLDivElement | null>;
|
||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function MobilePageContent({
|
||||
|
||||
@ -10,7 +10,7 @@ import Konva from "konva";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
|
||||
type DebugDrawingLayerProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
cameraWidth: number;
|
||||
cameraHeight: number;
|
||||
};
|
||||
|
||||
@ -17,7 +17,7 @@ type ObjectPathProps = {
|
||||
color?: number[];
|
||||
width?: number;
|
||||
pointRadius?: number;
|
||||
imgRef: React.RefObject<HTMLImageElement | null>;
|
||||
imgRef: React.RefObject<HTMLImageElement>;
|
||||
onPointClick?: (index: number) => void;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
@ -22,7 +22,6 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import type { JSX } from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
type PlatformAwareDialogProps = {
|
||||
|
||||
@ -91,7 +91,7 @@ export default function HlsVideoPlayer({
|
||||
|
||||
// playback
|
||||
|
||||
const hlsRef = useRef<Hls>(undefined);
|
||||
const hlsRef = useRef<Hls>();
|
||||
const [useHlsCompat, setUseHlsCompat] = useState(false);
|
||||
const [loadedMetadata, setLoadedMetadata] = useState(false);
|
||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||
|
||||
@ -51,10 +51,10 @@ export default function WebRtcPlayer({
|
||||
|
||||
// camera states
|
||||
|
||||
const pcRef = useRef<RTCPeerConnection | undefined>(undefined);
|
||||
const pcRef = useRef<RTCPeerConnection | undefined>();
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>(undefined);
|
||||
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const PeerConnection = useCallback(
|
||||
async (media: string) => {
|
||||
|
||||
@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil";
|
||||
import { usePolygonStates } from "@/hooks/use-polygon-states";
|
||||
|
||||
type PolygonCanvasProps = {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
camera: string;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
@ -18,7 +18,7 @@ import Konva from "konva";
|
||||
import { Vector2d } from "konva/lib/types";
|
||||
|
||||
type PolygonDrawerProps = {
|
||||
stageRef: RefObject<Konva.Stage | null>;
|
||||
stageRef: RefObject<Konva.Stage>;
|
||||
points: number[][];
|
||||
distances: number[];
|
||||
isActive: boolean;
|
||||
|
||||
@ -37,8 +37,8 @@ export type EventReviewTimelineProps = {
|
||||
events: ReviewSegment[];
|
||||
visibleTimestamps?: number[];
|
||||
severityType: ReviewSeverity;
|
||||
timelineRef?: RefObject<HTMLDivElement | null>;
|
||||
contentRef: RefObject<HTMLDivElement | null>;
|
||||
timelineRef?: RefObject<HTMLDivElement>;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||
isZooming: boolean;
|
||||
zoomDirection: TimelineZoomDirection;
|
||||
|
||||
@ -28,7 +28,7 @@ type EventSegmentProps = {
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
severityType: ReviewSeverity;
|
||||
contentRef: RefObject<HTMLDivElement | null>;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
||||
dense: boolean;
|
||||
|
||||
@ -41,8 +41,8 @@ export type MotionReviewTimelineProps = {
|
||||
events: ReviewSegment[];
|
||||
motion_events: MotionData[];
|
||||
noRecordingRanges?: RecordingSegment[];
|
||||
contentRef: RefObject<HTMLDivElement | null>;
|
||||
timelineRef?: RefObject<HTMLDivElement | null>;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
timelineRef?: RefObject<HTMLDivElement>;
|
||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||
dense?: boolean;
|
||||
isZooming: boolean;
|
||||
|
||||
@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
|
||||
export type ReviewTimelineProps = {
|
||||
timelineRef: RefObject<HTMLDivElement | null>;
|
||||
contentRef: RefObject<HTMLDivElement | null>;
|
||||
timelineRef: RefObject<HTMLDivElement>;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
segmentDuration: number;
|
||||
timelineDuration: number;
|
||||
timelineStartAligned: number;
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
|
||||
export type SummaryTimelineProps = {
|
||||
reviewTimelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
reviewTimelineRef: React.RefObject<HTMLDivElement>;
|
||||
timelineStart: number;
|
||||
timelineEnd: number;
|
||||
segmentDuration: number;
|
||||
|
||||
@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment";
|
||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
|
||||
type VirtualizedEventSegmentsProps = {
|
||||
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
timelineRef: React.RefObject<HTMLDivElement>;
|
||||
segments: number[];
|
||||
events: ReviewSegment[];
|
||||
segmentDuration: number;
|
||||
@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = {
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
severityType: ReviewSeverity;
|
||||
contentRef: React.RefObject<HTMLDivElement | null>;
|
||||
contentRef: React.RefObject<HTMLDivElement>;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
dense: boolean;
|
||||
alignStartDateToTimeline: (timestamp: number) => number;
|
||||
|
||||
@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment";
|
||||
import { ReviewSegment, MotionData } from "@/types/review";
|
||||
|
||||
type VirtualizedMotionSegmentsProps = {
|
||||
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
timelineRef: React.RefObject<HTMLDivElement>;
|
||||
segments: number[];
|
||||
events: ReviewSegment[];
|
||||
motion_events: MotionData[];
|
||||
@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = {
|
||||
showMinimap: boolean;
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
contentRef: React.RefObject<HTMLDivElement | null>;
|
||||
contentRef: React.RefObject<HTMLDivElement>;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
dense: boolean;
|
||||
motionOnly: boolean;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { JSX } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "./button";
|
||||
import { Calendar } from "./calendar";
|
||||
@ -125,8 +124,8 @@ export function DateRangePicker({
|
||||
);
|
||||
|
||||
// Refs to store the values of range and rangeCompare when the date picker is opened
|
||||
const openedRangeRef = useRef<DateRange | undefined>(undefined);
|
||||
const openedRangeCompareRef = useRef<DateRange | undefined>(undefined);
|
||||
const openedRangeRef = useRef<DateRange | undefined>();
|
||||
const openedRangeCompareRef = useRef<DateRange | undefined>();
|
||||
|
||||
const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
|
||||
undefined,
|
||||
|
||||
265
web/src/components/ui/carousel.tsx
Normal file
265
web/src/components/ui/carousel.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = "Carousel";
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = "CarouselContent";
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = "CarouselItem";
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
aria-label={t("trackingDetails.carousel.previous")}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">{t("trackingDetails.carousel.previous")}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = "CarouselPrevious";
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
aria-label={t("trackingDetails.carousel.next")}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">{t("trackingDetails.carousel.next")}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = "CarouselNext";
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
@ -33,9 +33,9 @@ const FormField = <
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext value={{ name: props.name }}>
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext>
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -77,9 +77,9 @@ const FormItem = React.forwardRef<
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext value={{ id }}>
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-1", className)} {...props} />
|
||||
</FormItemContext>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ForwardedRef, forwardRef } from "react";
|
||||
import { IconType } from "react-icons";
|
||||
|
||||
interface IconWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
interface IconWrapperProps {
|
||||
icon: IconType;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const IconWrapper = forwardRef(
|
||||
|
||||
@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef<
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext value={contextValue}>
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
@ -161,7 +161,7 @@ const SidebarProvider = React.forwardRef<
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -22,9 +22,9 @@ const ToggleGroup = React.forwardRef<
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext value={{ variant, size }}>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext>
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
))
|
||||
|
||||
|
||||
@ -102,5 +102,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
axios.get("/logout", { withCredentials: true });
|
||||
};
|
||||
|
||||
return <AuthContext value={{ auth, login, logout }}>{children}</AuthContext>;
|
||||
return (
|
||||
<AuthContext.Provider value={{ auth, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,7 +125,11 @@ export function DetailStreamProvider({
|
||||
isDetailMode,
|
||||
};
|
||||
|
||||
return <DetailStreamContext value={value}>{children}</DetailStreamContext>;
|
||||
return (
|
||||
<DetailStreamContext.Provider value={value}>
|
||||
{children}
|
||||
</DetailStreamContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
|
||||
@ -77,9 +77,9 @@ export function LanguageProvider({
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageProviderContext {...props} value={value}>
|
||||
<LanguageProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</LanguageProviderContext>
|
||||
</LanguageProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ThemeProvider } from "@/context/theme-provider";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { ApiProvider } from "@/api";
|
||||
import { IconContext } from "react-icons";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
@ -14,23 +15,25 @@ type TProvidersProps = {
|
||||
|
||||
function providers({ children }: TProvidersProps) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<ApiProvider>
|
||||
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
||||
<LanguageProvider>
|
||||
<TooltipProvider>
|
||||
<IconContext.Provider value={{ size: "20" }}>
|
||||
<StatusBarMessagesProvider>
|
||||
<StreamingSettingsProvider>
|
||||
{children}
|
||||
</StreamingSettingsProvider>
|
||||
</StatusBarMessagesProvider>
|
||||
</IconContext.Provider>
|
||||
</TooltipProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</ApiProvider>
|
||||
</AuthProvider>
|
||||
<RecoilRoot>
|
||||
<AuthProvider>
|
||||
<ApiProvider>
|
||||
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
||||
<LanguageProvider>
|
||||
<TooltipProvider>
|
||||
<IconContext.Provider value={{ size: "20" }}>
|
||||
<StatusBarMessagesProvider>
|
||||
<StreamingSettingsProvider>
|
||||
{children}
|
||||
</StreamingSettingsProvider>
|
||||
</StatusBarMessagesProvider>
|
||||
</IconContext.Provider>
|
||||
</TooltipProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</ApiProvider>
|
||||
</AuthProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -107,10 +107,10 @@ export function StatusBarMessagesProvider({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StatusBarMessagesContext
|
||||
<StatusBarMessagesContext.Provider
|
||||
value={{ messages, addMessage, removeMessage, clearMessages }}
|
||||
>
|
||||
{children}
|
||||
</StatusBarMessagesContext>
|
||||
</StatusBarMessagesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export function StreamingSettingsProvider({
|
||||
}, [allGroupsStreamingSettings, setPersistedGroupStreamingSettings]);
|
||||
|
||||
return (
|
||||
<StreamingSettingsContext
|
||||
<StreamingSettingsContext.Provider
|
||||
value={{
|
||||
allGroupsStreamingSettings,
|
||||
setAllGroupsStreamingSettings,
|
||||
@ -52,7 +52,7 @@ export function StreamingSettingsProvider({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StreamingSettingsContext>
|
||||
</StreamingSettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -124,9 +124,9 @@ export function ThemeProvider({
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext {...props} value={value}>
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext>
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -31,24 +31,25 @@ export function useResizeObserver(...refs: RefType[]) {
|
||||
[],
|
||||
);
|
||||
|
||||
// Resolve refs to actual DOM elements for use as stable effect dependencies.
|
||||
// Rest params create a new array each call, but the underlying elements are
|
||||
// stable DOM nodes, so spreading them into the dep array avoids re-running
|
||||
// the effect on every render.
|
||||
const elements = refs.map((ref) =>
|
||||
ref instanceof Window ? document.body : ref.current,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
elements.forEach((el) => {
|
||||
if (el) resizeObserver.observe(el);
|
||||
refs.forEach((ref) => {
|
||||
if (ref instanceof Window) {
|
||||
resizeObserver.observe(document.body);
|
||||
} else if (ref.current) {
|
||||
resizeObserver.observe(ref.current);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
refs.forEach((ref) => {
|
||||
if (ref instanceof Window) {
|
||||
resizeObserver.unobserve(document.body);
|
||||
} else if (ref.current) {
|
||||
resizeObserver.unobserve(ref.current);
|
||||
}
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...elements, resizeObserver]);
|
||||
}, [refs, resizeObserver]);
|
||||
|
||||
if (dimensions.length == refs.length) {
|
||||
return dimensions;
|
||||
|
||||
@ -5,7 +5,6 @@ import { LiveStreamMetadata } from "@/types/live";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 10000;
|
||||
const DEFER_DELAY_MS = 2000;
|
||||
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
|
||||
|
||||
/**
|
||||
* Hook that fetches go2rtc stream metadata with deferred loading.
|
||||
@ -78,7 +77,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
|
||||
return metadata;
|
||||
}, []);
|
||||
|
||||
const { data: metadata = EMPTY_METADATA } = useSWR<{
|
||||
const { data: metadata = {} } = useSWR<{
|
||||
[key: string]: LiveStreamMetadata;
|
||||
}>(swrKey, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
|
||||
import useUserInteraction from "./use-user-interaction";
|
||||
|
||||
type DraggableElementProps = {
|
||||
contentRef: React.RefObject<HTMLElement | null>;
|
||||
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
segmentsRef: React.RefObject<HTMLDivElement | null>;
|
||||
draggableElementRef: React.RefObject<HTMLDivElement | null>;
|
||||
contentRef: React.RefObject<HTMLElement>;
|
||||
timelineRef: React.RefObject<HTMLDivElement>;
|
||||
segmentsRef: React.RefObject<HTMLDivElement>;
|
||||
draggableElementRef: React.RefObject<HTMLDivElement>;
|
||||
segmentDuration: number;
|
||||
showDraggableElement: boolean;
|
||||
draggableElementTime?: number;
|
||||
|
||||
@ -78,7 +78,7 @@ function removeEventListeners(
|
||||
}
|
||||
|
||||
export function useFullscreen<T extends HTMLElement = HTMLElement>(
|
||||
elementRef: RefObject<T | null>,
|
||||
elementRef: RefObject<T>,
|
||||
) {
|
||||
const [fullscreen, setFullscreen] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const useImageLoaded = (): [
|
||||
React.RefObject<HTMLImageElement | null>,
|
||||
React.RefObject<HTMLImageElement>,
|
||||
boolean,
|
||||
() => void,
|
||||
] => {
|
||||
|
||||
@ -3,7 +3,7 @@ import { useCallback } from "react";
|
||||
export type TimelineUtilsProps = {
|
||||
segmentDuration: number;
|
||||
timelineDuration?: number;
|
||||
timelineRef?: React.RefObject<HTMLElement | null>;
|
||||
timelineRef?: React.RefObject<HTMLElement>;
|
||||
};
|
||||
|
||||
export function useTimelineUtils({
|
||||
|
||||
@ -11,7 +11,7 @@ type UseTimelineZoomProps = {
|
||||
zoomLevels: ZoomSettings[];
|
||||
onZoomChange: (newZoomLevel: number) => void;
|
||||
pinchThresholdPercent?: number;
|
||||
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
timelineRef: React.RefObject<HTMLDivElement>;
|
||||
timelineDuration: number;
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type UseUserInteractionProps = {
|
||||
elementRef: React.RefObject<HTMLElement | null>;
|
||||
elementRef: React.RefObject<HTMLElement>;
|
||||
};
|
||||
|
||||
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
||||
const [userInteracting, setUserInteracting] = useState(false);
|
||||
const interactionTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
const interactionTimeout = useRef<NodeJS.Timeout>();
|
||||
const isProgrammaticScroll = useRef(false);
|
||||
|
||||
const setProgrammaticScroll = useCallback(() => {
|
||||
|
||||
@ -7,7 +7,7 @@ export type VideoResolutionType = {
|
||||
};
|
||||
|
||||
export function useVideoDimensions(
|
||||
containerRef: React.RefObject<HTMLDivElement | null>,
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
) {
|
||||
const [{ width: containerWidth, height: containerHeight }] =
|
||||
useResizeObserver(containerRef);
|
||||
|
||||
@ -14,9 +14,10 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
ItemCallback,
|
||||
Layout,
|
||||
LayoutItem,
|
||||
ResponsiveGridLayout as Responsive,
|
||||
Responsive,
|
||||
WidthProvider,
|
||||
} from "react-grid-layout";
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "react-resizable/css/styles.css";
|
||||
@ -55,7 +56,7 @@ type DraggableGridLayoutProps = {
|
||||
cameras: CameraConfig[];
|
||||
cameraGroup: string;
|
||||
cameraRef: (node: HTMLElement | null) => void;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
includeBirdseye: boolean;
|
||||
onSelectCamera: (camera: string) => void;
|
||||
windowVisible: boolean;
|
||||
@ -115,8 +116,11 @@ export default function DraggableGridLayout({
|
||||
|
||||
// grid layout
|
||||
|
||||
const [gridLayout, setGridLayout, isGridLayoutLoaded] =
|
||||
useUserPersistence<Layout>(`${cameraGroup}-draggable-layout`);
|
||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||
|
||||
const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence<
|
||||
Layout[]
|
||||
>(`${cameraGroup}-draggable-layout`);
|
||||
|
||||
const [group] = useUserPersistedOverlayState(
|
||||
"cameraGroup",
|
||||
@ -154,11 +158,11 @@ export default function DraggableGridLayout({
|
||||
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
|
||||
useState<boolean>();
|
||||
const [currentGridLayout, setCurrentGridLayout] = useState<
|
||||
Layout | undefined
|
||||
Layout[] | undefined
|
||||
>();
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(currentLayout: Layout) => {
|
||||
(currentLayout: Layout[]) => {
|
||||
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
||||
return;
|
||||
}
|
||||
@ -170,7 +174,7 @@ export default function DraggableGridLayout({
|
||||
);
|
||||
|
||||
const generateLayout = useCallback(
|
||||
(baseLayout: Layout | undefined) => {
|
||||
(baseLayout: Layout[] | undefined) => {
|
||||
if (!isGridLayoutLoaded) {
|
||||
return;
|
||||
}
|
||||
@ -180,7 +184,7 @@ export default function DraggableGridLayout({
|
||||
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
|
||||
: cameras.map((camera) => camera?.name || "");
|
||||
|
||||
const optionsMap: LayoutItem[] = baseLayout
|
||||
const optionsMap: Layout[] = baseLayout
|
||||
? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
|
||||
: [];
|
||||
|
||||
@ -359,14 +363,12 @@ export default function DraggableGridLayout({
|
||||
);
|
||||
}, [availableWidth, marginValue]);
|
||||
|
||||
const handleResize = (
|
||||
_layout: Layout,
|
||||
oldLayoutItem: LayoutItem | null,
|
||||
layoutItem: LayoutItem | null,
|
||||
placeholder: LayoutItem | null,
|
||||
const handleResize: ItemCallback = (
|
||||
_: Layout[],
|
||||
oldLayoutItem: Layout,
|
||||
layoutItem: Layout,
|
||||
placeholder: Layout,
|
||||
) => {
|
||||
if (!oldLayoutItem || !layoutItem || !placeholder) return;
|
||||
|
||||
const heightDiff = layoutItem.h - oldLayoutItem.h;
|
||||
const widthDiff = layoutItem.w - oldLayoutItem.w;
|
||||
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
|
||||
@ -535,9 +537,8 @@ export default function DraggableGridLayout({
|
||||
currentGroups={groups}
|
||||
activeGroup={group}
|
||||
/>
|
||||
<Responsive
|
||||
<ResponsiveGridLayout
|
||||
className="grid-layout"
|
||||
width={availableWidth ?? window.innerWidth}
|
||||
layouts={{
|
||||
lg: currentGridLayout,
|
||||
md: currentGridLayout,
|
||||
@ -550,17 +551,13 @@ export default function DraggableGridLayout({
|
||||
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
||||
margin={[marginValue, marginValue]}
|
||||
containerPadding={[0, isEditMode ? 6 : 3]}
|
||||
resizeConfig={{
|
||||
enabled: isEditMode,
|
||||
handles: isEditMode ? ["sw", "nw", "se", "ne"] : [],
|
||||
}}
|
||||
dragConfig={{
|
||||
enabled: isEditMode,
|
||||
}}
|
||||
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
||||
onDragStop={handleLayoutChange}
|
||||
onResize={handleResize}
|
||||
onResizeStart={() => setShowCircles(false)}
|
||||
onResizeStop={handleLayoutChange}
|
||||
isDraggable={isEditMode}
|
||||
isResizable={isEditMode}
|
||||
>
|
||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||
<BirdseyeLivePlayerGridItem
|
||||
@ -688,7 +685,7 @@ export default function DraggableGridLayout({
|
||||
</GridLiveContextMenu>
|
||||
);
|
||||
})}
|
||||
</Responsive>
|
||||
</ResponsiveGridLayout>
|
||||
{isDesktop && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -419,7 +419,7 @@ export default function SearchView({
|
||||
>();
|
||||
|
||||
// keep track of previous ref to outline thumbnail when dialog closes
|
||||
const prevSearchDetailRef = useRef<SearchResult | undefined>(undefined);
|
||||
const prevSearchDetailRef = useRef<SearchResult | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchDetail === undefined && prevSearchDetailRef.current) {
|
||||
@ -619,9 +619,7 @@ export default function SearchView({
|
||||
return (
|
||||
<div
|
||||
key={value.id}
|
||||
ref={(item) => {
|
||||
itemRefs.current[index] = item;
|
||||
}}
|
||||
ref={(item) => (itemRefs.current[index] = item)}
|
||||
data-start={value.start_time}
|
||||
className="relative flex flex-col rounded-lg"
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user