mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 02:29:19 +03:00
Update frontend to React 19 (#22275)
* remove unused RecoilRoot and fix implicit ref callback
Remove the vestigial recoil dependency (zero consumers) and convert
the implicit-return ref callback in SearchView to block form to
prevent React 19 interpreting it as a cleanup function.
* replace react-transition-group with framer-motion in Chip
Replace CSSTransition with framer-motion AnimatePresence + motion.div
for React 19 compatibility (react-transition-group uses findDOMNode).
framer-motion is already a project dependency.
* migrate react-grid-layout v1 to v2
- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)
* upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch
- vaul: ^0.9.1 → ^1.1.2
- next-themes: ^0.3.0 → ^0.4.6
- framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support)
- react-zoom-pan-pinch: 3.4.4 → latest
* upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5
Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5
* upgrade typescript to 5.9.3
* modernize Context.Provider to React 19 shorthand
Replace <Context.Provider value={...}> with <Context value={...}>
across all project-owned context providers. External library contexts
(react-icons IconContext, radix TooltipPrimitive) left unchanged.
* add runtime patches for React 19 compatibility
- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
to prevent infinite render loops from unstable ref callbacks
https://github.com/radix-ui/primitives/issues/3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
SlotClone instead of inline composeRefs to prevent re-render cycles
https://github.com/radix-ui/primitives/pull/3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
cause "Maximum update depth exceeded" with React 19 auto-batching
https://github.com/facebook/react/issues/27613
- Add npm overrides to ensure single hoisted copies of compose-refs
and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency
* formatting
* use availableWidth instead of useContainerWidth for grid layout
The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.
* remove unused carousel component and fix React 19 peer deps
Remove embla-carousel-react and its unused Carousel UI component.
Upgrade sonner v1 → v2 for native React 19 support. Remove
@types/react-icons stub (react-icons bundles its own types).
These changes eliminate all peer dependency conflicts, so
npm install works without --legacy-peer-deps.
* fix React 19 infinite re-render loop on live dashboard
The "Maximum update depth exceeded" error was caused by two issues:
1. useDeferredStreamMetadata returned a new `{}` default on every render
when SWR data was undefined, creating an unstable reference that
triggered the useEffect in useCameraLiveMode on every render cycle.
Fixed by using a stable module-level EMPTY_METADATA constant.
2. useResizeObserver's rest parameter `...refs` created a new array on
every render, causing its useEffect to re-run and re-observe elements
continuously. Fixed by stabilizing refs with useRef and only
reconnecting the observer when actual DOM elements change.
This commit is contained in:
parent
b2118382cb
commit
2782931c72
4529
web/package-lock.json
generated
4529
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
"postinstall": "patch-package",
|
||||||
"build": "tsc && vite build --base=/BASE_PATH/",
|
"build": "tsc && vite build --base=/BASE_PATH/",
|
||||||
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .",
|
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .",
|
||||||
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
|
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
|
||||||
@ -32,7 +33,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slider": "^1.2.3",
|
"@radix-ui/react-slider": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.1.3",
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.2",
|
||||||
@ -50,8 +51,7 @@
|
|||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"embla-carousel-react": "^8.2.0",
|
"framer-motion": "^12.35.0",
|
||||||
"framer-motion": "^11.5.4",
|
|
||||||
"hls.js": "^1.5.20",
|
"hls.js": "^1.5.20",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"i18next-http-backend": "^3.0.1",
|
"i18next-http-backend": "^3.0.1",
|
||||||
@ -61,30 +61,28 @@
|
|||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.477.0",
|
||||||
"monaco-yaml": "^5.3.1",
|
"monaco-yaml": "^5.3.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.4.6",
|
||||||
"nosleep.js": "^0.12.0",
|
"nosleep.js": "^0.12.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.4",
|
||||||
"react-apexcharts": "^1.4.1",
|
"react-apexcharts": "^1.4.1",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.7.0",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.4",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-grid-layout": "^1.5.0",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-i18next": "^15.2.0",
|
"react-i18next": "^15.2.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-konva": "^18.2.10",
|
"react-konva": "^19.2.3",
|
||||||
"react-router-dom": "^6.30.3",
|
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"react-router-dom": "^6.30.3",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
"react-tracked": "^2.0.1",
|
"react-tracked": "^2.0.1",
|
||||||
"react-transition-group": "^4.4.5",
|
|
||||||
"react-use-websocket": "^4.8.1",
|
"react-use-websocket": "^4.8.1",
|
||||||
"react-zoom-pan-pinch": "3.4.4",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"recoil": "^0.7.7",
|
"remark-gfm": "^4.0.0",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^2.0.7",
|
||||||
"sort-by": "^1.2.0",
|
"sort-by": "^1.2.0",
|
||||||
"strftime": "^0.10.3",
|
"strftime": "^0.10.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
@ -92,7 +90,7 @@
|
|||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"use-long-press": "^3.2.0",
|
"use-long-press": "^3.2.0",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^1.1.2",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
@ -101,11 +99,8 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.2",
|
"@testing-library/jest-dom": "^6.6.2",
|
||||||
"@types/lodash": "^4.17.12",
|
"@types/lodash": "^4.17.12",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"@types/react": "^18.3.2",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@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",
|
"@types/strftime": "^0.9.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"@typescript-eslint/parser": "^7.5.0",
|
||||||
@ -116,19 +111,26 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-jest": "^28.2.0",
|
"eslint-plugin-jest": "^28.2.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.8",
|
"eslint-plugin-react-refresh": "^0.4.8",
|
||||||
"eslint-plugin-vitest-globals": "^1.5.0",
|
"eslint-plugin-vitest-globals": "^1.5.0",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
|
"monaco-editor": "^0.52.0",
|
||||||
"msw": "^2.3.5",
|
"msw": "^2.3.5",
|
||||||
|
"patch-package": "^8.0.1",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^6.4.1",
|
"vite": "^6.4.1",
|
||||||
"vitest": "^3.0.7"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
web/patches/@radix-ui+react-compose-refs+1.1.2.patch
Normal file
75
web/patches/@radix-ui+react-compose-refs+1.1.2.patch
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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,
|
||||||
46
web/patches/@radix-ui+react-slot+1.2.4.patch
Normal file
46
web/patches/@radix-ui+react-slot+1.2.4.patch
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
23
web/patches/react-use-websocket+4.8.1.patch
Normal file
23
web/patches/react-use-websocket+4.8.1.patch
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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,10 +114,17 @@ interface PropertyElement {
|
|||||||
content: React.ReactElement;
|
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) {
|
function isObjectLikeElement(item: PropertyElement) {
|
||||||
const fieldSchema = item.content.props?.schema as
|
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
|
||||||
| { type?: string | string[] }
|
|
||||||
| undefined;
|
|
||||||
return fieldSchema?.type === "object";
|
return fieldSchema?.type === "object";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,16 +170,21 @@ function GridLayoutObjectFieldTemplate(
|
|||||||
|
|
||||||
// Override the properties rendering with grid layout
|
// Override the properties rendering with grid layout
|
||||||
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
||||||
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
|
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
|
||||||
|
"hidden";
|
||||||
|
|
||||||
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
||||||
|
|
||||||
// Separate regular and advanced properties
|
// Separate regular and advanced properties
|
||||||
const advancedProps = visibleProps.filter(
|
const advancedProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
(p) =>
|
||||||
|
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||||
|
?.advanced === true,
|
||||||
);
|
);
|
||||||
const regularProps = visibleProps.filter(
|
const regularProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
(p) =>
|
||||||
|
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||||
|
?.advanced !== true,
|
||||||
);
|
);
|
||||||
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
||||||
isPathModified([...fieldPath, prop.name]),
|
isPathModified([...fieldPath, prop.name]),
|
||||||
|
|||||||
@ -448,6 +448,9 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const errorsProps = errors?.props as { errors?: unknown[] } | undefined;
|
||||||
|
const hasFieldErrors = !!errors && (errorsProps?.errors?.length ?? 0) > 0;
|
||||||
|
|
||||||
const renderStandardLabel = () => {
|
const renderStandardLabel = () => {
|
||||||
if (!shouldRenderStandardLabel) {
|
if (!shouldRenderStandardLabel) {
|
||||||
return null;
|
return null;
|
||||||
@ -459,7 +462,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium",
|
"text-sm font-medium",
|
||||||
isModified && "text-danger",
|
isModified && "text-danger",
|
||||||
errors && errors.props?.errors?.length > 0 && "text-destructive",
|
hasFieldErrors && "text-destructive",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{finalLabel}
|
{finalLabel}
|
||||||
@ -497,7 +500,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium",
|
"text-sm font-medium",
|
||||||
isModified && "text-danger",
|
isModified && "text-danger",
|
||||||
errors && errors.props?.errors?.length > 0 && "text-destructive",
|
hasFieldErrors && "text-destructive",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{finalLabel}
|
{finalLabel}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields
|
// Custom MultiSchemaFieldTemplate to handle anyOf [Type, null] fields
|
||||||
// Renders simple nullable types as single inputs instead of dropdowns
|
// Renders simple nullable types as single inputs instead of dropdowns
|
||||||
|
|
||||||
|
import type { JSX } from "react";
|
||||||
import {
|
import {
|
||||||
MultiSchemaFieldTemplateProps,
|
MultiSchemaFieldTemplateProps,
|
||||||
StrictRJSFSchema,
|
StrictRJSFSchema,
|
||||||
|
|||||||
@ -25,6 +25,15 @@ import {
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { AddPropertyButton, AdvancedCollapsible } from "../components";
|
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) {
|
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
@ -182,16 +191,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
uiSchema?.["ui:options"]?.disableNestedCard === true;
|
uiSchema?.["ui:options"]?.disableNestedCard === true;
|
||||||
|
|
||||||
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
||||||
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
|
(prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] ===
|
||||||
|
"hidden";
|
||||||
|
|
||||||
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
||||||
|
|
||||||
// Check for advanced section grouping
|
// Check for advanced section grouping
|
||||||
const advancedProps = visibleProps.filter(
|
const advancedProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
(p) =>
|
||||||
|
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||||
|
?.advanced === true,
|
||||||
);
|
);
|
||||||
const regularProps = visibleProps.filter(
|
const regularProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
(p) =>
|
||||||
|
(p.content.props as RjsfElementProps).uiSchema?.["ui:options"]
|
||||||
|
?.advanced !== true,
|
||||||
);
|
);
|
||||||
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
const hasModifiedAdvanced = advancedProps.some((prop) =>
|
||||||
checkSubtreeModified([...fieldPath, prop.name]),
|
checkSubtreeModified([...fieldPath, prop.name]),
|
||||||
@ -333,9 +347,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
|
|
||||||
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
||||||
const isObjectLikeField = (item: (typeof properties)[number]) => {
|
const isObjectLikeField = (item: (typeof properties)[number]) => {
|
||||||
const fieldSchema = item.content.props.schema as
|
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
|
||||||
| { type?: string | string[] }
|
|
||||||
| undefined;
|
|
||||||
return fieldSchema?.type === "object";
|
return fieldSchema?.type === "object";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import type { JSX } from "react";
|
||||||
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
import { FunctionComponent, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
interface IProp {
|
interface IProp {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LogSeverity } from "@/types/log";
|
import { LogSeverity } from "@/types/log";
|
||||||
import { ReactNode, useMemo, useRef } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { isIOS } from "react-device-detect";
|
import { isIOS } from "react-device-detect";
|
||||||
import { CSSTransition } from "react-transition-group";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
type ChipProps = {
|
type ChipProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -17,39 +17,31 @@ export default function Chip({
|
|||||||
in: inProp = true,
|
in: inProp = true,
|
||||||
onClick,
|
onClick,
|
||||||
}: ChipProps) {
|
}: ChipProps) {
|
||||||
const nodeRef = useRef(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition
|
<AnimatePresence>
|
||||||
in={inProp}
|
{inProp && (
|
||||||
nodeRef={nodeRef}
|
<motion.div
|
||||||
timeout={500}
|
initial={{ opacity: 0 }}
|
||||||
classNames={{
|
animate={{ opacity: 1 }}
|
||||||
enter: "opacity-0",
|
exit={{ opacity: 0 }}
|
||||||
enterActive: "opacity-100 transition-opacity duration-500 ease-in-out",
|
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||||
exit: "opacity-100",
|
className={cn(
|
||||||
exitActive: "opacity-0 transition-opacity duration-500 ease-in-out",
|
"flex items-center rounded-2xl px-2 py-1.5",
|
||||||
}}
|
className,
|
||||||
unmountOnExit
|
!isIOS && "z-10",
|
||||||
>
|
)}
|
||||||
<div
|
onClick={(e) => {
|
||||||
ref={nodeRef}
|
e.stopPropagation();
|
||||||
className={cn(
|
|
||||||
"flex items-center rounded-2xl px-2 py-1.5",
|
|
||||||
className,
|
|
||||||
!isIOS && "z-10",
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</motion.div>
|
||||||
</CSSTransition>
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,9 +55,9 @@ export function MobilePage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
<MobilePageContext value={{ open, onOpenChange: setOpen }}>
|
||||||
{children}
|
{children}
|
||||||
</MobilePageContext.Provider>
|
</MobilePageContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ export function MobilePagePortal({
|
|||||||
type MobilePageContentProps = {
|
type MobilePageContentProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
scrollerRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobilePageContent({
|
export function MobilePageContent({
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import Konva from "konva";
|
|||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
|
||||||
type DebugDrawingLayerProps = {
|
type DebugDrawingLayerProps = {
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
cameraWidth: number;
|
cameraWidth: number;
|
||||||
cameraHeight: number;
|
cameraHeight: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type ObjectPathProps = {
|
|||||||
color?: number[];
|
color?: number[];
|
||||||
width?: number;
|
width?: number;
|
||||||
pointRadius?: number;
|
pointRadius?: number;
|
||||||
imgRef: React.RefObject<HTMLImageElement>;
|
imgRef: React.RefObject<HTMLImageElement | null>;
|
||||||
onPointClick?: (index: number) => void;
|
onPointClick?: (index: number) => void;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
import type { JSX } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
type PlatformAwareDialogProps = {
|
type PlatformAwareDialogProps = {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export default function HlsVideoPlayer({
|
|||||||
|
|
||||||
// playback
|
// playback
|
||||||
|
|
||||||
const hlsRef = useRef<Hls>();
|
const hlsRef = useRef<Hls>(undefined);
|
||||||
const [useHlsCompat, setUseHlsCompat] = useState(false);
|
const [useHlsCompat, setUseHlsCompat] = useState(false);
|
||||||
const [loadedMetadata, setLoadedMetadata] = useState(false);
|
const [loadedMetadata, setLoadedMetadata] = useState(false);
|
||||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||||
|
|||||||
@ -51,10 +51,10 @@ export default function WebRtcPlayer({
|
|||||||
|
|
||||||
// camera states
|
// camera states
|
||||||
|
|
||||||
const pcRef = useRef<RTCPeerConnection | undefined>();
|
const pcRef = useRef<RTCPeerConnection | undefined>(undefined);
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||||
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>();
|
const videoLoadTimeoutRef = useRef<NodeJS.Timeout>(undefined);
|
||||||
|
|
||||||
const PeerConnection = useCallback(
|
const PeerConnection = useCallback(
|
||||||
async (media: string) => {
|
async (media: string) => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { snapPointToLines } from "@/utils/canvasUtil";
|
|||||||
import { usePolygonStates } from "@/hooks/use-polygon-states";
|
import { usePolygonStates } from "@/hooks/use-polygon-states";
|
||||||
|
|
||||||
type PolygonCanvasProps = {
|
type PolygonCanvasProps = {
|
||||||
containerRef: RefObject<HTMLDivElement>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
camera: string;
|
camera: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import Konva from "konva";
|
|||||||
import { Vector2d } from "konva/lib/types";
|
import { Vector2d } from "konva/lib/types";
|
||||||
|
|
||||||
type PolygonDrawerProps = {
|
type PolygonDrawerProps = {
|
||||||
stageRef: RefObject<Konva.Stage>;
|
stageRef: RefObject<Konva.Stage | null>;
|
||||||
points: number[][];
|
points: number[][];
|
||||||
distances: number[];
|
distances: number[];
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|||||||
@ -37,8 +37,8 @@ export type EventReviewTimelineProps = {
|
|||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
visibleTimestamps?: number[];
|
visibleTimestamps?: number[];
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
timelineRef?: RefObject<HTMLDivElement>;
|
timelineRef?: RefObject<HTMLDivElement | null>;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement | null>;
|
||||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||||
isZooming: boolean;
|
isZooming: boolean;
|
||||||
zoomDirection: TimelineZoomDirection;
|
zoomDirection: TimelineZoomDirection;
|
||||||
|
|||||||
@ -28,7 +28,7 @@ type EventSegmentProps = {
|
|||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
minimapEndTime?: number;
|
minimapEndTime?: number;
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement | null>;
|
||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
||||||
dense: boolean;
|
dense: boolean;
|
||||||
|
|||||||
@ -41,8 +41,8 @@ export type MotionReviewTimelineProps = {
|
|||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
motion_events: MotionData[];
|
motion_events: MotionData[];
|
||||||
noRecordingRanges?: RecordingSegment[];
|
noRecordingRanges?: RecordingSegment[];
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement | null>;
|
||||||
timelineRef?: RefObject<HTMLDivElement>;
|
timelineRef?: RefObject<HTMLDivElement | null>;
|
||||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
isZooming: boolean;
|
isZooming: boolean;
|
||||||
|
|||||||
@ -20,8 +20,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
export type ReviewTimelineProps = {
|
export type ReviewTimelineProps = {
|
||||||
timelineRef: RefObject<HTMLDivElement>;
|
timelineRef: RefObject<HTMLDivElement | null>;
|
||||||
contentRef: RefObject<HTMLDivElement>;
|
contentRef: RefObject<HTMLDivElement | null>;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
timelineDuration: number;
|
timelineDuration: number;
|
||||||
timelineStartAligned: number;
|
timelineStartAligned: number;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
|
|
||||||
export type SummaryTimelineProps = {
|
export type SummaryTimelineProps = {
|
||||||
reviewTimelineRef: React.RefObject<HTMLDivElement>;
|
reviewTimelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
timelineStart: number;
|
timelineStart: number;
|
||||||
timelineEnd: number;
|
timelineEnd: number;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { EventSegment } from "./EventSegment";
|
|||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
|
|
||||||
type VirtualizedEventSegmentsProps = {
|
type VirtualizedEventSegmentsProps = {
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
segments: number[];
|
segments: number[];
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
@ -19,7 +19,7 @@ type VirtualizedEventSegmentsProps = {
|
|||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
minimapEndTime?: number;
|
minimapEndTime?: number;
|
||||||
severityType: ReviewSeverity;
|
severityType: ReviewSeverity;
|
||||||
contentRef: React.RefObject<HTMLDivElement>;
|
contentRef: React.RefObject<HTMLDivElement | null>;
|
||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
dense: boolean;
|
dense: boolean;
|
||||||
alignStartDateToTimeline: (timestamp: number) => number;
|
alignStartDateToTimeline: (timestamp: number) => number;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import MotionSegment from "./MotionSegment";
|
|||||||
import { ReviewSegment, MotionData } from "@/types/review";
|
import { ReviewSegment, MotionData } from "@/types/review";
|
||||||
|
|
||||||
type VirtualizedMotionSegmentsProps = {
|
type VirtualizedMotionSegmentsProps = {
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
segments: number[];
|
segments: number[];
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
motion_events: MotionData[];
|
motion_events: MotionData[];
|
||||||
@ -19,7 +19,7 @@ type VirtualizedMotionSegmentsProps = {
|
|||||||
showMinimap: boolean;
|
showMinimap: boolean;
|
||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
minimapEndTime?: number;
|
minimapEndTime?: number;
|
||||||
contentRef: React.RefObject<HTMLDivElement>;
|
contentRef: React.RefObject<HTMLDivElement | null>;
|
||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
dense: boolean;
|
dense: boolean;
|
||||||
motionOnly: boolean;
|
motionOnly: boolean;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { JSX } from "react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
import { Calendar } from "./calendar";
|
import { Calendar } from "./calendar";
|
||||||
@ -124,8 +125,8 @@ export function DateRangePicker({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Refs to store the values of range and rangeCompare when the date picker is opened
|
// Refs to store the values of range and rangeCompare when the date picker is opened
|
||||||
const openedRangeRef = useRef<DateRange | undefined>();
|
const openedRangeRef = useRef<DateRange | undefined>(undefined);
|
||||||
const openedRangeCompareRef = useRef<DateRange | undefined>();
|
const openedRangeCompareRef = useRef<DateRange | undefined>(undefined);
|
||||||
|
|
||||||
const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
|
const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@ -1,265 +0,0 @@
|
|||||||
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
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,9 +77,9 @@ const FormItem = React.forwardRef<
|
|||||||
const id = React.useId();
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext value={{ id }}>
|
||||||
<div ref={ref} className={cn("space-y-1", className)} {...props} />
|
<div ref={ref} className={cn("space-y-1", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormItem.displayName = "FormItem";
|
FormItem.displayName = "FormItem";
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { ForwardedRef, forwardRef } from "react";
|
import { ForwardedRef, forwardRef } from "react";
|
||||||
import { IconType } from "react-icons";
|
import { IconType } from "react-icons";
|
||||||
|
|
||||||
interface IconWrapperProps {
|
interface IconWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
icon: IconType;
|
icon: IconType;
|
||||||
className?: string;
|
className?: string;
|
||||||
[key: string]: any;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconWrapper = forwardRef(
|
const IconWrapper = forwardRef(
|
||||||
|
|||||||
@ -141,7 +141,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext value={contextValue}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
@ -161,7 +161,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,9 +22,9 @@ const ToggleGroup = React.forwardRef<
|
|||||||
className={cn("flex items-center justify-center gap-1", className)}
|
className={cn("flex items-center justify-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
<ToggleGroupContext value={{ variant, size }}>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@ -102,9 +102,5 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
axios.get("/logout", { withCredentials: true });
|
axios.get("/logout", { withCredentials: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AuthContext value={{ auth, login, logout }}>{children}</AuthContext>;
|
||||||
<AuthContext.Provider value={{ auth, login, logout }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,11 +125,7 @@ export function DetailStreamProvider({
|
|||||||
isDetailMode,
|
isDetailMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <DetailStreamContext value={value}>{children}</DetailStreamContext>;
|
||||||
<DetailStreamContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</DetailStreamContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
|||||||
@ -77,9 +77,9 @@ export function LanguageProvider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageProviderContext.Provider {...props} value={value}>
|
<LanguageProviderContext {...props} value={value}>
|
||||||
{children}
|
{children}
|
||||||
</LanguageProviderContext.Provider>
|
</LanguageProviderContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { ThemeProvider } from "@/context/theme-provider";
|
import { ThemeProvider } from "@/context/theme-provider";
|
||||||
import { RecoilRoot } from "recoil";
|
|
||||||
import { ApiProvider } from "@/api";
|
import { ApiProvider } from "@/api";
|
||||||
import { IconContext } from "react-icons";
|
import { IconContext } from "react-icons";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
@ -15,25 +14,23 @@ type TProvidersProps = {
|
|||||||
|
|
||||||
function providers({ children }: TProvidersProps) {
|
function providers({ children }: TProvidersProps) {
|
||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<AuthProvider>
|
||||||
<AuthProvider>
|
<ApiProvider>
|
||||||
<ApiProvider>
|
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
||||||
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
<LanguageProvider>
|
||||||
<LanguageProvider>
|
<TooltipProvider>
|
||||||
<TooltipProvider>
|
<IconContext.Provider value={{ size: "20" }}>
|
||||||
<IconContext.Provider value={{ size: "20" }}>
|
<StatusBarMessagesProvider>
|
||||||
<StatusBarMessagesProvider>
|
<StreamingSettingsProvider>
|
||||||
<StreamingSettingsProvider>
|
{children}
|
||||||
{children}
|
</StreamingSettingsProvider>
|
||||||
</StreamingSettingsProvider>
|
</StatusBarMessagesProvider>
|
||||||
</StatusBarMessagesProvider>
|
</IconContext.Provider>
|
||||||
</IconContext.Provider>
|
</TooltipProvider>
|
||||||
</TooltipProvider>
|
</LanguageProvider>
|
||||||
</LanguageProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</ApiProvider>
|
||||||
</ApiProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,10 +107,10 @@ export function StatusBarMessagesProvider({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBarMessagesContext.Provider
|
<StatusBarMessagesContext
|
||||||
value={{ messages, addMessage, removeMessage, clearMessages }}
|
value={{ messages, addMessage, removeMessage, clearMessages }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StatusBarMessagesContext.Provider>
|
</StatusBarMessagesContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function StreamingSettingsProvider({
|
|||||||
}, [allGroupsStreamingSettings, setPersistedGroupStreamingSettings]);
|
}, [allGroupsStreamingSettings, setPersistedGroupStreamingSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StreamingSettingsContext.Provider
|
<StreamingSettingsContext
|
||||||
value={{
|
value={{
|
||||||
allGroupsStreamingSettings,
|
allGroupsStreamingSettings,
|
||||||
setAllGroupsStreamingSettings,
|
setAllGroupsStreamingSettings,
|
||||||
@ -52,7 +52,7 @@ export function StreamingSettingsProvider({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StreamingSettingsContext.Provider>
|
</StreamingSettingsContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -124,9 +124,9 @@ export function ThemeProvider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProviderContext.Provider {...props} value={value}>
|
<ThemeProviderContext {...props} value={value}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeProviderContext.Provider>
|
</ThemeProviderContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,25 +31,24 @@ 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(() => {
|
useEffect(() => {
|
||||||
refs.forEach((ref) => {
|
elements.forEach((el) => {
|
||||||
if (ref instanceof Window) {
|
if (el) resizeObserver.observe(el);
|
||||||
resizeObserver.observe(document.body);
|
|
||||||
} else if (ref.current) {
|
|
||||||
resizeObserver.observe(ref.current);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
refs.forEach((ref) => {
|
resizeObserver.disconnect();
|
||||||
if (ref instanceof Window) {
|
|
||||||
resizeObserver.unobserve(document.body);
|
|
||||||
} else if (ref.current) {
|
|
||||||
resizeObserver.unobserve(ref.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}, [refs, resizeObserver]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [...elements, resizeObserver]);
|
||||||
|
|
||||||
if (dimensions.length == refs.length) {
|
if (dimensions.length == refs.length) {
|
||||||
return dimensions;
|
return dimensions;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { LiveStreamMetadata } from "@/types/live";
|
|||||||
|
|
||||||
const FETCH_TIMEOUT_MS = 10000;
|
const FETCH_TIMEOUT_MS = 10000;
|
||||||
const DEFER_DELAY_MS = 2000;
|
const DEFER_DELAY_MS = 2000;
|
||||||
|
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that fetches go2rtc stream metadata with deferred loading.
|
* Hook that fetches go2rtc stream metadata with deferred loading.
|
||||||
@ -77,7 +78,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: metadata = {} } = useSWR<{
|
const { data: metadata = EMPTY_METADATA } = useSWR<{
|
||||||
[key: string]: LiveStreamMetadata;
|
[key: string]: LiveStreamMetadata;
|
||||||
}>(swrKey, fetcher, {
|
}>(swrKey, fetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import useUserInteraction from "./use-user-interaction";
|
import useUserInteraction from "./use-user-interaction";
|
||||||
|
|
||||||
type DraggableElementProps = {
|
type DraggableElementProps = {
|
||||||
contentRef: React.RefObject<HTMLElement>;
|
contentRef: React.RefObject<HTMLElement | null>;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
segmentsRef: React.RefObject<HTMLDivElement>;
|
segmentsRef: React.RefObject<HTMLDivElement | null>;
|
||||||
draggableElementRef: React.RefObject<HTMLDivElement>;
|
draggableElementRef: React.RefObject<HTMLDivElement | null>;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
showDraggableElement: boolean;
|
showDraggableElement: boolean;
|
||||||
draggableElementTime?: number;
|
draggableElementTime?: number;
|
||||||
|
|||||||
@ -78,7 +78,7 @@ function removeEventListeners(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useFullscreen<T extends HTMLElement = HTMLElement>(
|
export function useFullscreen<T extends HTMLElement = HTMLElement>(
|
||||||
elementRef: RefObject<T>,
|
elementRef: RefObject<T | null>,
|
||||||
) {
|
) {
|
||||||
const [fullscreen, setFullscreen] = useState<boolean>(false);
|
const [fullscreen, setFullscreen] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const useImageLoaded = (): [
|
const useImageLoaded = (): [
|
||||||
React.RefObject<HTMLImageElement>,
|
React.RefObject<HTMLImageElement | null>,
|
||||||
boolean,
|
boolean,
|
||||||
() => void,
|
() => void,
|
||||||
] => {
|
] => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useCallback } from "react";
|
|||||||
export type TimelineUtilsProps = {
|
export type TimelineUtilsProps = {
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
timelineDuration?: number;
|
timelineDuration?: number;
|
||||||
timelineRef?: React.RefObject<HTMLElement>;
|
timelineRef?: React.RefObject<HTMLElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTimelineUtils({
|
export function useTimelineUtils({
|
||||||
|
|||||||
@ -11,7 +11,7 @@ type UseTimelineZoomProps = {
|
|||||||
zoomLevels: ZoomSettings[];
|
zoomLevels: ZoomSettings[];
|
||||||
onZoomChange: (newZoomLevel: number) => void;
|
onZoomChange: (newZoomLevel: number) => void;
|
||||||
pinchThresholdPercent?: number;
|
pinchThresholdPercent?: number;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||||
timelineDuration: number;
|
timelineDuration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
type UseUserInteractionProps = {
|
type UseUserInteractionProps = {
|
||||||
elementRef: React.RefObject<HTMLElement>;
|
elementRef: React.RefObject<HTMLElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
||||||
const [userInteracting, setUserInteracting] = useState(false);
|
const [userInteracting, setUserInteracting] = useState(false);
|
||||||
const interactionTimeout = useRef<NodeJS.Timeout>();
|
const interactionTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||||
const isProgrammaticScroll = useRef(false);
|
const isProgrammaticScroll = useRef(false);
|
||||||
|
|
||||||
const setProgrammaticScroll = useCallback(() => {
|
const setProgrammaticScroll = useCallback(() => {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export type VideoResolutionType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useVideoDimensions(
|
export function useVideoDimensions(
|
||||||
containerRef: React.RefObject<HTMLDivElement>,
|
containerRef: React.RefObject<HTMLDivElement | null>,
|
||||||
) {
|
) {
|
||||||
const [{ width: containerWidth, height: containerHeight }] =
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
useResizeObserver(containerRef);
|
useResizeObserver(containerRef);
|
||||||
|
|||||||
@ -14,10 +14,9 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
ItemCallback,
|
|
||||||
Layout,
|
Layout,
|
||||||
Responsive,
|
LayoutItem,
|
||||||
WidthProvider,
|
ResponsiveGridLayout as Responsive,
|
||||||
} from "react-grid-layout";
|
} from "react-grid-layout";
|
||||||
import "react-grid-layout/css/styles.css";
|
import "react-grid-layout/css/styles.css";
|
||||||
import "react-resizable/css/styles.css";
|
import "react-resizable/css/styles.css";
|
||||||
@ -56,7 +55,7 @@ type DraggableGridLayoutProps = {
|
|||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
cameraGroup: string;
|
cameraGroup: string;
|
||||||
cameraRef: (node: HTMLElement | null) => void;
|
cameraRef: (node: HTMLElement | null) => void;
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
includeBirdseye: boolean;
|
includeBirdseye: boolean;
|
||||||
onSelectCamera: (camera: string) => void;
|
onSelectCamera: (camera: string) => void;
|
||||||
windowVisible: boolean;
|
windowVisible: boolean;
|
||||||
@ -116,11 +115,8 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
// grid layout
|
// grid layout
|
||||||
|
|
||||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
const [gridLayout, setGridLayout, isGridLayoutLoaded] =
|
||||||
|
useUserPersistence<Layout>(`${cameraGroup}-draggable-layout`);
|
||||||
const [gridLayout, setGridLayout, isGridLayoutLoaded] = useUserPersistence<
|
|
||||||
Layout[]
|
|
||||||
>(`${cameraGroup}-draggable-layout`);
|
|
||||||
|
|
||||||
const [group] = useUserPersistedOverlayState(
|
const [group] = useUserPersistedOverlayState(
|
||||||
"cameraGroup",
|
"cameraGroup",
|
||||||
@ -158,11 +154,11 @@ export default function DraggableGridLayout({
|
|||||||
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
|
const [currentIncludeBirdseye, setCurrentIncludeBirdseye] =
|
||||||
useState<boolean>();
|
useState<boolean>();
|
||||||
const [currentGridLayout, setCurrentGridLayout] = useState<
|
const [currentGridLayout, setCurrentGridLayout] = useState<
|
||||||
Layout[] | undefined
|
Layout | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(
|
const handleLayoutChange = useCallback(
|
||||||
(currentLayout: Layout[]) => {
|
(currentLayout: Layout) => {
|
||||||
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
if (!isGridLayoutLoaded || !isEqual(gridLayout, currentGridLayout)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -174,7 +170,7 @@ export default function DraggableGridLayout({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const generateLayout = useCallback(
|
const generateLayout = useCallback(
|
||||||
(baseLayout: Layout[] | undefined) => {
|
(baseLayout: Layout | undefined) => {
|
||||||
if (!isGridLayoutLoaded) {
|
if (!isGridLayoutLoaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -184,7 +180,7 @@ export default function DraggableGridLayout({
|
|||||||
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
|
? ["birdseye", ...cameras.map((camera) => camera?.name || "")]
|
||||||
: cameras.map((camera) => camera?.name || "");
|
: cameras.map((camera) => camera?.name || "");
|
||||||
|
|
||||||
const optionsMap: Layout[] = baseLayout
|
const optionsMap: LayoutItem[] = baseLayout
|
||||||
? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
|
? baseLayout.filter((layout) => cameraNames?.includes(layout.i))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@ -363,12 +359,14 @@ export default function DraggableGridLayout({
|
|||||||
);
|
);
|
||||||
}, [availableWidth, marginValue]);
|
}, [availableWidth, marginValue]);
|
||||||
|
|
||||||
const handleResize: ItemCallback = (
|
const handleResize = (
|
||||||
_: Layout[],
|
_layout: Layout,
|
||||||
oldLayoutItem: Layout,
|
oldLayoutItem: LayoutItem | null,
|
||||||
layoutItem: Layout,
|
layoutItem: LayoutItem | null,
|
||||||
placeholder: Layout,
|
placeholder: LayoutItem | null,
|
||||||
) => {
|
) => {
|
||||||
|
if (!oldLayoutItem || !layoutItem || !placeholder) return;
|
||||||
|
|
||||||
const heightDiff = layoutItem.h - oldLayoutItem.h;
|
const heightDiff = layoutItem.h - oldLayoutItem.h;
|
||||||
const widthDiff = layoutItem.w - oldLayoutItem.w;
|
const widthDiff = layoutItem.w - oldLayoutItem.w;
|
||||||
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
|
const changeCoef = oldLayoutItem.w / oldLayoutItem.h;
|
||||||
@ -537,8 +535,9 @@ export default function DraggableGridLayout({
|
|||||||
currentGroups={groups}
|
currentGroups={groups}
|
||||||
activeGroup={group}
|
activeGroup={group}
|
||||||
/>
|
/>
|
||||||
<ResponsiveGridLayout
|
<Responsive
|
||||||
className="grid-layout"
|
className="grid-layout"
|
||||||
|
width={availableWidth ?? window.innerWidth}
|
||||||
layouts={{
|
layouts={{
|
||||||
lg: currentGridLayout,
|
lg: currentGridLayout,
|
||||||
md: currentGridLayout,
|
md: currentGridLayout,
|
||||||
@ -551,13 +550,17 @@ export default function DraggableGridLayout({
|
|||||||
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
cols={{ lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 }}
|
||||||
margin={[marginValue, marginValue]}
|
margin={[marginValue, marginValue]}
|
||||||
containerPadding={[0, isEditMode ? 6 : 3]}
|
containerPadding={[0, isEditMode ? 6 : 3]}
|
||||||
resizeHandles={isEditMode ? ["sw", "nw", "se", "ne"] : []}
|
resizeConfig={{
|
||||||
|
enabled: isEditMode,
|
||||||
|
handles: isEditMode ? ["sw", "nw", "se", "ne"] : [],
|
||||||
|
}}
|
||||||
|
dragConfig={{
|
||||||
|
enabled: isEditMode,
|
||||||
|
}}
|
||||||
onDragStop={handleLayoutChange}
|
onDragStop={handleLayoutChange}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
onResizeStart={() => setShowCircles(false)}
|
onResizeStart={() => setShowCircles(false)}
|
||||||
onResizeStop={handleLayoutChange}
|
onResizeStop={handleLayoutChange}
|
||||||
isDraggable={isEditMode}
|
|
||||||
isResizable={isEditMode}
|
|
||||||
>
|
>
|
||||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||||
<BirdseyeLivePlayerGridItem
|
<BirdseyeLivePlayerGridItem
|
||||||
@ -685,7 +688,7 @@ export default function DraggableGridLayout({
|
|||||||
</GridLiveContextMenu>
|
</GridLiveContextMenu>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ResponsiveGridLayout>
|
</Responsive>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -419,7 +419,7 @@ export default function SearchView({
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
// keep track of previous ref to outline thumbnail when dialog closes
|
// keep track of previous ref to outline thumbnail when dialog closes
|
||||||
const prevSearchDetailRef = useRef<SearchResult | undefined>();
|
const prevSearchDetailRef = useRef<SearchResult | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchDetail === undefined && prevSearchDetailRef.current) {
|
if (searchDetail === undefined && prevSearchDetailRef.current) {
|
||||||
@ -619,7 +619,9 @@ export default function SearchView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={value.id}
|
key={value.id}
|
||||||
ref={(item) => (itemRefs.current[index] = item)}
|
ref={(item) => {
|
||||||
|
itemRefs.current[index] = item;
|
||||||
|
}}
|
||||||
data-start={value.start_time}
|
data-start={value.start_time}
|
||||||
className="relative flex flex-col rounded-lg"
|
className="relative flex flex-col rounded-lg"
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user