mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Miscellaneous Fixes (#21050)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Don't add to history when opening search dialog * Update caniuse * Revamp the history handling for dialog components * clarify audio transcription docs * Use titlecase helper * Allow running object clasasification on stationary objects * small spacing tweaks for tablets * require admin role to delete users * explicitly prevent deletion of admin user --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
parent
e79ff9a079
commit
de2144f158
@ -75,7 +75,13 @@ audio:
|
|||||||
|
|
||||||
### Audio Transcription
|
### Audio Transcription
|
||||||
|
|
||||||
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI’s open-source Whisper models via `faster-whisper`. To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
|
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI’s open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
|
||||||
|
|
||||||
|
Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service.
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
audio_transcription:
|
audio_transcription:
|
||||||
|
|||||||
@ -578,8 +578,14 @@ def create_user(
|
|||||||
return JSONResponse(content={"username": body.username})
|
return JSONResponse(content={"username": body.username})
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/users/{username}")
|
@router.delete("/users/{username}", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def delete_user(username: str):
|
def delete_user(request: Request, username: str):
|
||||||
|
# Prevent deletion of the built-in admin user
|
||||||
|
if username == "admin":
|
||||||
|
return JSONResponse(
|
||||||
|
content={"message": "Cannot delete admin user"}, status_code=403
|
||||||
|
)
|
||||||
|
|
||||||
User.delete_by_id(username)
|
User.delete_by_id(username)
|
||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from typing import Any
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
from titlecase import titlecase
|
||||||
|
|
||||||
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
|
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
from frigate.comms.inter_process import InterProcessRequestor
|
||||||
@ -455,14 +456,14 @@ def run_analysis(
|
|||||||
|
|
||||||
for i, verified_label in enumerate(final_data["data"]["verified_objects"]):
|
for i, verified_label in enumerate(final_data["data"]["verified_objects"]):
|
||||||
object_type = verified_label.replace("-verified", "").replace("_", " ")
|
object_type = verified_label.replace("-verified", "").replace("_", " ")
|
||||||
name = sub_labels_list[i].replace("_", " ").title()
|
name = titlecase(sub_labels_list[i].replace("_", " "))
|
||||||
unified_objects.append(f"{name} ({object_type})")
|
unified_objects.append(f"{name} ({object_type})")
|
||||||
|
|
||||||
for label in objects_list:
|
for label in objects_list:
|
||||||
if "-verified" in label:
|
if "-verified" in label:
|
||||||
continue
|
continue
|
||||||
elif label in labelmap_objects:
|
elif label in labelmap_objects:
|
||||||
object_type = label.replace("_", " ").title()
|
object_type = titlecase(label.replace("_", " "))
|
||||||
|
|
||||||
if label in attribute_labels:
|
if label in attribute_labels:
|
||||||
unified_objects.append(f"{object_type} (delivery/service)")
|
unified_objects.append(f"{object_type} (delivery/service)")
|
||||||
|
|||||||
@ -405,9 +405,6 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
if obj_data.get("end_time") is not None:
|
if obj_data.get("end_time") is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if obj_data.get("stationary"):
|
|
||||||
return
|
|
||||||
|
|
||||||
object_id = obj_data["id"]
|
object_id = obj_data["id"]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -4702,9 +4702,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001651",
|
"version": "1.0.30001757",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||||
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
|
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { isPWA } from "@/utils/isPWA";
|
import { isPWA } from "@/utils/isPWA";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
const MobilePageContext = createContext<{
|
const MobilePageContext = createContext<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -24,15 +24,16 @@ type MobilePageProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
enableHistoryBack?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobilePage({
|
export function MobilePage({
|
||||||
children,
|
children,
|
||||||
open: controlledOpen,
|
open: controlledOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
enableHistoryBack = true,
|
||||||
}: MobilePageProps) {
|
}: MobilePageProps) {
|
||||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const open = controlledOpen ?? uncontrolledOpen;
|
const open = controlledOpen ?? uncontrolledOpen;
|
||||||
const setOpen = useCallback(
|
const setOpen = useCallback(
|
||||||
@ -46,33 +47,12 @@ export function MobilePage({
|
|||||||
[onOpenChange, setUncontrolledOpen],
|
[onOpenChange, setUncontrolledOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Handle browser back button to close mobile page
|
||||||
let isActive = true;
|
useHistoryBack({
|
||||||
|
enabled: enableHistoryBack,
|
||||||
if (open && isActive) {
|
open,
|
||||||
window.history.pushState({ isMobilePage: true }, "", location.pathname);
|
onClose: () => setOpen(false),
|
||||||
}
|
});
|
||||||
|
|
||||||
const handlePopState = (event: PopStateEvent) => {
|
|
||||||
if (open && isActive) {
|
|
||||||
event.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
// Delay replaceState to ensure state updates are processed
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isActive) {
|
|
||||||
window.history.replaceState(null, "", location.pathname);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("popstate", handlePopState);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
window.removeEventListener("popstate", handlePopState);
|
|
||||||
};
|
|
||||||
}, [open, setOpen, location.pathname]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>
|
||||||
|
|||||||
@ -113,7 +113,12 @@ export function PlatformAwareSheet({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
<Sheet
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
modal={false}
|
||||||
|
enableHistoryBack
|
||||||
|
>
|
||||||
<SheetTrigger asChild className={triggerClassName}>
|
<SheetTrigger asChild className={triggerClassName}>
|
||||||
{trigger}
|
{trigger}
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as React from "react";
|
|||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
// Enhanced Dialog with History Support
|
// Enhanced Dialog with History Support
|
||||||
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
|
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
|
||||||
@ -15,51 +16,28 @@ const Dialog = ({
|
|||||||
...props
|
...props
|
||||||
}: HistoryDialogProps) => {
|
}: HistoryDialogProps) => {
|
||||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||||
const historyStateRef = React.useRef<null | {
|
|
||||||
listener: (e: PopStateEvent) => void;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
|
// Sync internal state with controlled open prop
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open !== undefined) {
|
if (open !== undefined) {
|
||||||
setInternalOpen(open);
|
setInternalOpen(open);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleOpenChange = React.useCallback(
|
||||||
if (enableHistoryBack) {
|
(newOpen: boolean) => {
|
||||||
if (internalOpen) {
|
setInternalOpen(newOpen);
|
||||||
window.history.pushState({ dialogOpen: true }, "");
|
onOpenChange?.(newOpen);
|
||||||
|
},
|
||||||
const listener = () => {
|
[onOpenChange],
|
||||||
setInternalOpen(false);
|
|
||||||
if (onOpenChange) onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
historyStateRef.current = { listener };
|
|
||||||
window.addEventListener("popstate", listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (internalOpen) {
|
|
||||||
window.removeEventListener("popstate", listener);
|
|
||||||
historyStateRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (historyStateRef.current) {
|
|
||||||
window.removeEventListener(
|
|
||||||
"popstate",
|
|
||||||
historyStateRef.current.listener,
|
|
||||||
);
|
);
|
||||||
historyStateRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [enableHistoryBack, internalOpen, onOpenChange]);
|
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
// Handle browser back button to close dialog
|
||||||
setInternalOpen(open);
|
useHistoryBack({
|
||||||
if (onOpenChange) {
|
enabled: enableHistoryBack,
|
||||||
onOpenChange(open);
|
open: internalOpen,
|
||||||
}
|
onClose: () => handleOpenChange(false),
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Root
|
<DialogPrimitive.Root
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useHistoryBack } from "@/hooks/use-history-back";
|
||||||
|
|
||||||
// Enhanced Sheet with History Support
|
// Enhanced Sheet with History Support
|
||||||
interface HistorySheetProps extends SheetPrimitive.DialogProps {
|
interface HistorySheetProps extends SheetPrimitive.DialogProps {
|
||||||
@ -17,51 +18,28 @@ const Sheet = ({
|
|||||||
...props
|
...props
|
||||||
}: HistorySheetProps) => {
|
}: HistorySheetProps) => {
|
||||||
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
const [internalOpen, setInternalOpen] = React.useState(open || false);
|
||||||
const historyStateRef = React.useRef<null | {
|
|
||||||
listener: (e: PopStateEvent) => void;
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
|
// Sync internal state with controlled open prop
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open !== undefined) {
|
if (open !== undefined) {
|
||||||
setInternalOpen(open);
|
setInternalOpen(open);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleOpenChange = React.useCallback(
|
||||||
if (enableHistoryBack) {
|
(newOpen: boolean) => {
|
||||||
if (internalOpen) {
|
setInternalOpen(newOpen);
|
||||||
window.history.pushState({ sheetOpen: true }, "");
|
onOpenChange?.(newOpen);
|
||||||
|
},
|
||||||
const listener = () => {
|
[onOpenChange],
|
||||||
setInternalOpen(false);
|
|
||||||
if (onOpenChange) onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
historyStateRef.current = { listener };
|
|
||||||
window.addEventListener("popstate", listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (internalOpen) {
|
|
||||||
window.removeEventListener("popstate", listener);
|
|
||||||
historyStateRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (historyStateRef.current) {
|
|
||||||
window.removeEventListener(
|
|
||||||
"popstate",
|
|
||||||
historyStateRef.current.listener,
|
|
||||||
);
|
);
|
||||||
historyStateRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [enableHistoryBack, internalOpen, onOpenChange]);
|
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
// Handle browser back button to close sheet
|
||||||
setInternalOpen(open);
|
useHistoryBack({
|
||||||
if (onOpenChange) {
|
enabled: enableHistoryBack,
|
||||||
onOpenChange(open);
|
open: internalOpen,
|
||||||
}
|
onClose: () => handleOpenChange(false),
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Root
|
<SheetPrimitive.Root
|
||||||
|
|||||||
57
web/src/hooks/use-history-back.ts
Normal file
57
web/src/hooks/use-history-back.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface UseHistoryBackOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that manages browser history for overlay components (dialogs, sheets, etc.)
|
||||||
|
* When enabled, pressing the browser back button will close the overlay instead of navigating away.
|
||||||
|
*/
|
||||||
|
export function useHistoryBack({
|
||||||
|
enabled,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: UseHistoryBackOptions): void {
|
||||||
|
const historyPushedRef = React.useRef(false);
|
||||||
|
const closedByBackRef = React.useRef(false);
|
||||||
|
|
||||||
|
// Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes
|
||||||
|
const onCloseRef = React.useRef(onClose);
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
// Only push history state if we haven't already (prevents duplicates in strict mode)
|
||||||
|
if (!historyPushedRef.current) {
|
||||||
|
window.history.pushState({ overlayOpen: true }, "");
|
||||||
|
historyPushedRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePopState = () => {
|
||||||
|
closedByBackRef.current = true;
|
||||||
|
historyPushedRef.current = false;
|
||||||
|
onCloseRef.current();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handlePopState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handlePopState);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Overlay is closing - clean up history if we pushed and it wasn't via back button
|
||||||
|
if (historyPushedRef.current && !closedByBackRef.current) {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
historyPushedRef.current = false;
|
||||||
|
closedByBackRef.current = false;
|
||||||
|
}
|
||||||
|
}, [enabled, open]);
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
import { getTranslatedLabel } from "@/utils/i18n";
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type MasksAndZoneViewProps = {
|
type MasksAndZoneViewProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
@ -697,7 +698,10 @@ export default function MasksAndZonesView({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex max-h-[50%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow"
|
className={cn(
|
||||||
|
"flex max-h-[50%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
|
||||||
|
isDesktop && "md:mr-3",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex size-full flex-row justify-center">
|
<div className="mx-auto flex size-full flex-row justify-center">
|
||||||
{cameraConfig &&
|
{cameraConfig &&
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import { LuExternalLink } from "react-icons/lu";
|
|||||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
|
||||||
type MotionTunerViewProps = {
|
type MotionTunerViewProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
@ -325,7 +327,12 @@ export default function MotionTunerView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{cameraConfig ? (
|
{cameraConfig ? (
|
||||||
<div className="flex max-h-[70%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex max-h-[70%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
|
||||||
|
isDesktop && "md:mr-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="size-full min-h-10">
|
<div className="size-full min-h-10">
|
||||||
<AutoUpdatingCameraImage
|
<AutoUpdatingCameraImage
|
||||||
camera={cameraConfig.name}
|
camera={cameraConfig.name}
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import { useTriggers } from "@/api/ws";
|
|||||||
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
||||||
import { CiCircleAlert } from "react-icons/ci";
|
import { CiCircleAlert } from "react-icons/ci";
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
|
||||||
type ConfigSetBody = {
|
type ConfigSetBody = {
|
||||||
requires_restart: number;
|
requires_restart: number;
|
||||||
@ -440,7 +441,12 @@ export default function TriggerView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2",
|
||||||
|
isDesktop && "order-none mr-3 mt-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!isSemanticSearchEnabled ? (
|
{!isSemanticSearchEnabled ? (
|
||||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
@ -651,7 +657,7 @@ export default function TriggerView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Table View */}
|
{/* Desktop Table View */}
|
||||||
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:mr-3 md:block">
|
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:block">
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="sticky top-0 bg-muted/50">
|
<TableHeader className="sticky top-0 bg-muted/50">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user