mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 03:41:55 +03:00
Miscellaneous fixes (#23295)
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
* filter motion review by allowed cameras * filter alertCameras by allowed cameras so the recent alerts query for restricted roles doesn't reference cameras they can't access * skip data streams in chapter exports to avoid ffmpeg segfault * formatting * restrict debug replay UI entry points to admin users * Adjust default iGPU name when it can't be found * Fix when model tries to request an invalid camera * Improve prompt * add collapsible main nav items in settings --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
d556ff8df2
commit
ec44398b1c
@ -547,9 +547,21 @@ async def _execute_get_live_context(
|
|||||||
camera: str,
|
camera: str,
|
||||||
allowed_cameras: List[str],
|
allowed_cameras: List[str],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
# Reject wildcards explicitly so models retry with a real camera name
|
||||||
|
# instead of silently fanning out across every camera.
|
||||||
|
if camera in ("*", "all"):
|
||||||
|
return {
|
||||||
|
"error": (
|
||||||
|
"get_live_context requires a single camera name; wildcards "
|
||||||
|
"are not supported. Call this tool once per camera."
|
||||||
|
),
|
||||||
|
"available_cameras": allowed_cameras,
|
||||||
|
}
|
||||||
|
|
||||||
if camera not in allowed_cameras:
|
if camera not in allowed_cameras:
|
||||||
return {
|
return {
|
||||||
"error": f"Camera '{camera}' not found or access denied",
|
"error": f"Camera '{camera}' not found or access denied",
|
||||||
|
"available_cameras": allowed_cameras,
|
||||||
}
|
}
|
||||||
|
|
||||||
if camera not in request.app.frigate_config.cameras:
|
if camera not in request.app.frigate_config.cameras:
|
||||||
@ -721,7 +733,14 @@ async def _execute_tool_internal(
|
|||||||
"Arguments: %s",
|
"Arguments: %s",
|
||||||
json.dumps(arguments),
|
json.dumps(arguments),
|
||||||
)
|
)
|
||||||
return {"error": "Camera parameter is required"}
|
return {
|
||||||
|
"error": (
|
||||||
|
"get_live_context requires a single camera name; "
|
||||||
|
"wildcards and empty values are not supported. "
|
||||||
|
"Call this tool once per camera."
|
||||||
|
),
|
||||||
|
"available_cameras": allowed_cameras,
|
||||||
|
}
|
||||||
return await _execute_get_live_context(request, camera, allowed_cameras)
|
return await _execute_get_live_context(request, camera, allowed_cameras)
|
||||||
elif tool_name == "start_camera_watch":
|
elif tool_name == "start_camera_watch":
|
||||||
return await _execute_start_camera_watch(request, arguments)
|
return await _execute_start_camera_watch(request, arguments)
|
||||||
|
|||||||
@ -518,16 +518,21 @@ def get_tool_definitions(
|
|||||||
"function": {
|
"function": {
|
||||||
"name": "get_live_context",
|
"name": "get_live_context",
|
||||||
"description": (
|
"description": (
|
||||||
"Get the current live image and detection information for a camera: objects being tracked, "
|
"Get the current live image and detection information for a single camera: objects being tracked, "
|
||||||
"zones, timestamps. Use this to understand what is visible in the live view. "
|
"zones, timestamps. Use this to understand what is visible in the live view. "
|
||||||
"Call this when answering questions about what is happening right now on a specific camera."
|
"Call this when answering questions about what is happening right now on a specific camera. "
|
||||||
|
"Operates on one camera at a time; call the tool again for each additional camera. "
|
||||||
|
"Wildcards and empty values are not accepted."
|
||||||
),
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"camera": {
|
"camera": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Camera name to get live context for.",
|
"description": (
|
||||||
|
"Exact name of a single camera to get live context for. "
|
||||||
|
"Wildcards (e.g. '*', 'all') and empty strings are not accepted."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["camera"],
|
"required": ["camera"],
|
||||||
|
|||||||
@ -579,7 +579,9 @@ class RecordingExporter(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
chapters_path = self._build_chapter_metadata_file(recordings)
|
chapters_path = self._build_chapter_metadata_file(recordings)
|
||||||
chapter_args = (
|
chapter_args = (
|
||||||
f" -i {chapters_path} -map 0 -map_metadata 1" if chapters_path else ""
|
f" -i {chapters_path} -map 0 -dn -map_metadata 1"
|
||||||
|
if chapters_path
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
ffmpeg_cmd = (
|
ffmpeg_cmd = (
|
||||||
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart"
|
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart"
|
||||||
|
|||||||
@ -478,7 +478,7 @@ def get_intel_gpu_stats(
|
|||||||
overall_pct = min(100.0, compute_pct + dec_pct)
|
overall_pct = min(100.0, compute_pct + dec_pct)
|
||||||
|
|
||||||
entry: dict[str, Any] = {
|
entry: dict[str, Any] = {
|
||||||
"name": names.get(pdev) or f"Intel GPU {pdev}",
|
"name": names.get(pdev) or "Intel iGPU",
|
||||||
"vendor": "intel",
|
"vendor": "intel",
|
||||||
"gpu": f"{round(overall_pct, 2)}%",
|
"gpu": f"{round(overall_pct, 2)}%",
|
||||||
"mem": "-%",
|
"mem": "-%",
|
||||||
|
|||||||
@ -130,9 +130,15 @@ export default function SearchResultActions({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("dialog.toast.error", { error: errorMessage }), {
|
toast.error(
|
||||||
position: "top-center",
|
t("dialog.toast.error", {
|
||||||
});
|
ns: "views/replay",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -206,7 +212,7 @@ export default function SearchResultActions({
|
|||||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{searchResult.has_clip && (
|
{isAdmin && searchResult.has_clip && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
aria-label={t("itemMenu.debugReplay.aria")}
|
aria-label={t("itemMenu.debugReplay.aria")}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { FaFilm } from "react-icons/fa6";
|
import { FaFilm } from "react-icons/fa6";
|
||||||
|
|
||||||
type ActionsDropdownProps = {
|
type ActionsDropdownProps = {
|
||||||
onDebugReplayClick: () => void;
|
onDebugReplayClick?: () => void;
|
||||||
onExportClick: () => void;
|
onExportClick: () => void;
|
||||||
onShareTimestampClick: () => void;
|
onShareTimestampClick: () => void;
|
||||||
};
|
};
|
||||||
@ -42,9 +42,11 @@ export default function ActionsDropdown({
|
|||||||
<DropdownMenuItem onClick={onShareTimestampClick}>
|
<DropdownMenuItem onClick={onShareTimestampClick}>
|
||||||
{t("recording.shareTimestamp.label", { ns: "components/dialog" })}
|
{t("recording.shareTimestamp.label", { ns: "components/dialog" })}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={onDebugReplayClick}>
|
{onDebugReplayClick && (
|
||||||
{t("title", { ns: "views/replay" })}
|
<DropdownMenuItem onClick={onDebugReplayClick}>
|
||||||
</DropdownMenuItem>
|
{t("title", { ns: "views/replay" })}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { StartExportResponse } from "@/types/export";
|
import { StartExportResponse } from "@/types/export";
|
||||||
import { ShareTimestampContent } from "./ShareTimestampDialog";
|
import { ShareTimestampContent } from "./ShareTimestampDialog";
|
||||||
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
|
|
||||||
type DrawerMode =
|
type DrawerMode =
|
||||||
| "none"
|
| "none"
|
||||||
@ -109,6 +110,7 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
"views/replay",
|
"views/replay",
|
||||||
"common",
|
"common",
|
||||||
]);
|
]);
|
||||||
|
const isAdmin = useIsAdmin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
||||||
const [exportTab, setExportTab] = useState<ExportTab>("export");
|
const [exportTab, setExportTab] = useState<ExportTab>("export");
|
||||||
@ -388,7 +390,7 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
{t("filter")}
|
{t("filter")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{features.includes("debug-replay") && (
|
{isAdmin && features.includes("debug-replay") && (
|
||||||
<Button
|
<Button
|
||||||
className="flex w-full items-center justify-center gap-2"
|
className="flex w-full items-center justify-center gap-2"
|
||||||
aria-label={t("title", { ns: "views/replay" })}
|
aria-label={t("title", { ns: "views/replay" })}
|
||||||
|
|||||||
@ -95,9 +95,15 @@ export default function DetailActionsMenu({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("dialog.toast.error", { error: errorMessage }), {
|
toast.error(
|
||||||
position: "top-center",
|
t("dialog.toast.error", {
|
||||||
});
|
ns: "views/replay",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -229,7 +235,7 @@ export default function DetailActionsMenu({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{search.has_clip && (
|
{isAdmin && search.has_clip && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
aria-label={t("itemMenu.debugReplay.aria")}
|
aria-label={t("itemMenu.debugReplay.aria")}
|
||||||
|
|||||||
@ -94,9 +94,15 @@ export default function EventMenu({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("dialog.toast.error", { error: errorMessage }), {
|
toast.error(
|
||||||
position: "top-center",
|
t("dialog.toast.error", {
|
||||||
});
|
ns: "views/replay",
|
||||||
|
error: errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -177,7 +183,7 @@ export default function EventMenu({
|
|||||||
{t("itemMenu.findSimilar.label")}
|
{t("itemMenu.findSimilar.label")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{event.has_clip && (
|
{isAdmin && event.has_clip && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
disabled={isStarting}
|
disabled={isStarting}
|
||||||
|
|||||||
@ -16,6 +16,11 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -589,7 +594,7 @@ function MobileMenuItem({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-10 w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md px-4 py-2 pr-2 text-sm font-medium text-primary-variant disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex h-10 w-full cursor-pointer items-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium text-primary-variant disabled:pointer-events-none disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -600,7 +605,6 @@ function MobileMenuItem({
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{label ?? <div>{t("menu." + item.key)}</div>}
|
{label ?? <div>{t("menu." + item.key)}</div>}
|
||||||
</div>
|
</div>
|
||||||
<LuChevronRight className="size-4" />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -613,6 +617,39 @@ export default function Settings() {
|
|||||||
const [sectionStatusByKey, setSectionStatusByKey] = useState<
|
const [sectionStatusByKey, setSectionStatusByKey] = useState<
|
||||||
Partial<Record<SettingsType, SectionStatus>>
|
Partial<Record<SettingsType, SectionStatus>>
|
||||||
>({});
|
>({});
|
||||||
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(
|
||||||
|
() =>
|
||||||
|
// all collapsed by default
|
||||||
|
new Set(
|
||||||
|
settingsGroups.filter((g) => g.items.length > 1).map((g) => g.label),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleGroupCollapsed = useCallback((label: string) => {
|
||||||
|
setCollapsedGroups((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(label)) {
|
||||||
|
next.delete(label);
|
||||||
|
} else {
|
||||||
|
next.add(label);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-expand the group containing the active page whenever pageToggle changes
|
||||||
|
useEffect(() => {
|
||||||
|
const containingGroup = settingsGroups.find((group) =>
|
||||||
|
group.items.some((item) => item.key === pageToggle),
|
||||||
|
);
|
||||||
|
if (!containingGroup) return;
|
||||||
|
setCollapsedGroups((prev) => {
|
||||||
|
if (!prev.has(containingGroup.label)) return prev;
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(containingGroup.label);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [pageToggle]);
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const { data: profilesData } = useSWR<ProfilesApiResponse>("profiles");
|
const { data: profilesData } = useSWR<ProfilesApiResponse>("profiles");
|
||||||
@ -1611,34 +1648,49 @@ export default function Settings() {
|
|||||||
visibleSettingsViews.includes(item.key as SettingsType),
|
visibleSettingsViews.includes(item.key as SettingsType),
|
||||||
);
|
);
|
||||||
if (filteredItems.length === 0) return null;
|
if (filteredItems.length === 0) return null;
|
||||||
|
const isMultiItem = filteredItems.length > 1;
|
||||||
|
const renderedExpanded =
|
||||||
|
!isMultiItem || !collapsedGroups.has(group.label);
|
||||||
|
const items = filteredItems.map((item) => (
|
||||||
|
<MobileMenuItem
|
||||||
|
key={item.key}
|
||||||
|
item={item}
|
||||||
|
className={cn(filteredItems.length == 1 && "pl-2")}
|
||||||
|
label={renderMenuItemLabel(item.key as SettingsType)}
|
||||||
|
onSelect={(key) => {
|
||||||
|
if (
|
||||||
|
!isAdmin &&
|
||||||
|
!ALLOWED_VIEWS_FOR_VIEWER.includes(key as SettingsType)
|
||||||
|
) {
|
||||||
|
setPageToggle("uiSettings");
|
||||||
|
} else {
|
||||||
|
setPageToggle(key as SettingsType);
|
||||||
|
}
|
||||||
|
setContentMobileOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
return (
|
return (
|
||||||
<div key={group.label} className="mb-3">
|
<div key={group.label} className="mb-3">
|
||||||
{filteredItems.length > 1 && (
|
{isMultiItem ? (
|
||||||
<h3 className="mb-2 ml-2 text-sm font-medium text-secondary-foreground">
|
<Collapsible
|
||||||
<div>{t("menu." + group.label)}</div>
|
open={renderedExpanded}
|
||||||
</h3>
|
onOpenChange={() => toggleGroupCollapsed(group.label)}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger className="flex min-h-10 w-full items-center justify-between rounded-md py-2 pl-2 pr-2 text-sm font-medium text-secondary-foreground">
|
||||||
|
<div>{t("menu." + group.label)}</div>
|
||||||
|
<LuChevronRight
|
||||||
|
className={cn(
|
||||||
|
"size-4 shrink-0 transition-transform duration-200",
|
||||||
|
renderedExpanded && "rotate-90",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>{items}</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
) : (
|
||||||
|
items
|
||||||
)}
|
)}
|
||||||
{filteredItems.map((item) => (
|
|
||||||
<MobileMenuItem
|
|
||||||
key={item.key}
|
|
||||||
item={item}
|
|
||||||
className={cn(filteredItems.length == 1 && "pl-2")}
|
|
||||||
label={renderMenuItemLabel(item.key as SettingsType)}
|
|
||||||
onSelect={(key) => {
|
|
||||||
if (
|
|
||||||
!isAdmin &&
|
|
||||||
!ALLOWED_VIEWS_FOR_VIEWER.includes(
|
|
||||||
key as SettingsType,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setPageToggle("uiSettings");
|
|
||||||
} else {
|
|
||||||
setPageToggle(key as SettingsType);
|
|
||||||
}
|
|
||||||
setContentMobileOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -1940,48 +1992,74 @@ export default function Settings() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
) : (
|
) : (
|
||||||
<>
|
(() => {
|
||||||
<SidebarGroupLabel
|
const hasActiveItem = filteredItems.some(
|
||||||
className={cn(
|
(item) => pageToggle === item.key,
|
||||||
"ml-2 cursor-default pl-0 text-sm",
|
);
|
||||||
filteredItems.some(
|
const renderedExpanded = !collapsedGroups.has(
|
||||||
(item) => pageToggle === item.key,
|
group.label,
|
||||||
)
|
);
|
||||||
? "text-primary"
|
return (
|
||||||
: "text-sidebar-foreground/80",
|
<Collapsible
|
||||||
)}
|
open={renderedExpanded}
|
||||||
>
|
onOpenChange={() =>
|
||||||
<div>{t("menu." + group.label)}</div>
|
toggleGroupCollapsed(group.label)
|
||||||
</SidebarGroupLabel>
|
}
|
||||||
<SidebarMenuSub className="mx-2 border-0">
|
>
|
||||||
{filteredItems.map((item) => (
|
<SidebarGroupLabel
|
||||||
<SidebarMenuSubItem key={item.key}>
|
asChild
|
||||||
<SidebarMenuSubButton
|
className={cn(
|
||||||
className="h-auto w-full py-1.5"
|
"ml-2 pl-0 text-sm",
|
||||||
isActive={pageToggle === item.key}
|
hasActiveItem
|
||||||
onClick={() => {
|
? "text-primary"
|
||||||
if (
|
: "text-sidebar-foreground/80",
|
||||||
!isAdmin &&
|
)}
|
||||||
!ALLOWED_VIEWS_FOR_VIEWER.includes(
|
>
|
||||||
item.key as SettingsType,
|
<CollapsibleTrigger className="flex w-full items-center justify-between">
|
||||||
)
|
<div>{t("menu." + group.label)}</div>
|
||||||
) {
|
<LuChevronRight
|
||||||
setPageToggle("uiSettings");
|
className={cn(
|
||||||
} else {
|
"size-4 shrink-0 transition-transform duration-200",
|
||||||
setPageToggle(item.key as SettingsType);
|
renderedExpanded && "rotate-90",
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="w-full cursor-pointer">
|
|
||||||
{renderMenuItemLabel(
|
|
||||||
item.key as SettingsType,
|
|
||||||
)}
|
)}
|
||||||
</div>
|
/>
|
||||||
</SidebarMenuSubButton>
|
</CollapsibleTrigger>
|
||||||
</SidebarMenuSubItem>
|
</SidebarGroupLabel>
|
||||||
))}
|
<CollapsibleContent>
|
||||||
</SidebarMenuSub>
|
<SidebarMenuSub className="mx-2 border-0 md:mx-0">
|
||||||
</>
|
{filteredItems.map((item) => (
|
||||||
|
<SidebarMenuSubItem key={item.key}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
className="h-auto w-full py-1.5"
|
||||||
|
isActive={pageToggle === item.key}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
!isAdmin &&
|
||||||
|
!ALLOWED_VIEWS_FOR_VIEWER.includes(
|
||||||
|
item.key as SettingsType,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setPageToggle("uiSettings");
|
||||||
|
} else {
|
||||||
|
setPageToggle(
|
||||||
|
item.key as SettingsType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full cursor-pointer">
|
||||||
|
{renderMenuItemLabel(
|
||||||
|
item.key as SettingsType,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})()
|
||||||
)}
|
)}
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -66,6 +66,7 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
|||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
import VideoControls from "@/components/player/VideoControls";
|
import VideoControls from "@/components/player/VideoControls";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||||
import {
|
import {
|
||||||
useCameraMotionNextTimestamp,
|
useCameraMotionNextTimestamp,
|
||||||
useCameraMotionOnlyRanges,
|
useCameraMotionOnlyRanges,
|
||||||
@ -1008,27 +1009,29 @@ function MotionReview({
|
|||||||
const { t } = useTranslation(["views/events", "common"]);
|
const { t } = useTranslation(["views/events", "common"]);
|
||||||
const segmentDuration = 30;
|
const segmentDuration = 30;
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const allowedCameras = useAllowedCameras();
|
||||||
|
|
||||||
const reviewCameras = useMemo(() => {
|
const reviewCameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let cameras;
|
const selectedCams = filter?.cameras;
|
||||||
if (!filter || !filter.cameras) {
|
const cameras = Object.values(config.cameras).filter((cam) => {
|
||||||
cameras = Object.values(config.cameras).filter(
|
if (isReplayCamera(cam.name)) {
|
||||||
(cam) => !isReplayCamera(cam.name),
|
return false;
|
||||||
);
|
}
|
||||||
} else {
|
if (!allowedCameras.includes(cam.name)) {
|
||||||
const filteredCams = filter.cameras;
|
return false;
|
||||||
|
}
|
||||||
cameras = Object.values(config.cameras).filter(
|
if (selectedCams && !selectedCams.includes(cam.name)) {
|
||||||
(cam) => filteredCams.includes(cam.name) && !isReplayCamera(cam.name),
|
return false;
|
||||||
);
|
}
|
||||||
}
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return cameras.sort((a, b) => a.ui.order - b.ui.order);
|
return cameras.sort((a, b) => a.ui.order - b.ui.order);
|
||||||
}, [config, filter]);
|
}, [config, filter, allowedCameras]);
|
||||||
|
|
||||||
const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({});
|
const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({});
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||||
import {
|
import {
|
||||||
AllGroupsStreamingSettings,
|
AllGroupsStreamingSettings,
|
||||||
@ -90,6 +91,7 @@ export default function LiveDashboardView({
|
|||||||
// recent events
|
// recent events
|
||||||
|
|
||||||
const eventUpdate = useFrigateReviews();
|
const eventUpdate = useFrigateReviews();
|
||||||
|
const allowedCameras = useAllowedCameras();
|
||||||
|
|
||||||
const alertCameras = useMemo(() => {
|
const alertCameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -98,14 +100,16 @@ export default function LiveDashboardView({
|
|||||||
|
|
||||||
if (cameraGroup == "default") {
|
if (cameraGroup == "default") {
|
||||||
return Object.values(config.cameras)
|
return Object.values(config.cameras)
|
||||||
.filter((cam) => cam.ui.dashboard)
|
.filter((cam) => cam.ui.dashboard && allowedCameras.includes(cam.name))
|
||||||
.map((cam) => cam.name)
|
.map((cam) => cam.name)
|
||||||
.join(",");
|
.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeBirdseye && cameras.length == 0) {
|
if (includeBirdseye && cameras.length == 0) {
|
||||||
return Object.values(config.cameras)
|
return Object.values(config.cameras)
|
||||||
.filter((cam) => cam.birdseye.enabled)
|
.filter(
|
||||||
|
(cam) => cam.birdseye.enabled && allowedCameras.includes(cam.name),
|
||||||
|
)
|
||||||
.map((cam) => cam.name)
|
.map((cam) => cam.name)
|
||||||
.join(",");
|
.join(",");
|
||||||
}
|
}
|
||||||
@ -114,7 +118,7 @@ export default function LiveDashboardView({
|
|||||||
.map((cam) => cam.name)
|
.map((cam) => cam.name)
|
||||||
.filter((cam) => config.camera_groups[cameraGroup]?.cameras.includes(cam))
|
.filter((cam) => config.camera_groups[cameraGroup]?.cameras.includes(cam))
|
||||||
.join(",");
|
.join(",");
|
||||||
}, [cameras, cameraGroup, config, includeBirdseye]);
|
}, [cameras, cameraGroup, config, includeBirdseye, allowedCameras]);
|
||||||
|
|
||||||
const { data: allEvents, mutate: updateEvents } = useSWR<ReviewSegment[]>([
|
const { data: allEvents, mutate: updateEvents } = useSWR<ReviewSegment[]>([
|
||||||
"review",
|
"review",
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import {
|
|||||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { TimeRange, TimelineType } from "@/types/timeline";
|
import { TimeRange, TimelineType } from "@/types/timeline";
|
||||||
import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer";
|
import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer";
|
||||||
@ -109,6 +110,7 @@ export function RecordingView({
|
|||||||
}: RecordingViewProps) {
|
}: RecordingViewProps) {
|
||||||
const { t } = useTranslation(["views/events", "components/dialog"]);
|
const { t } = useTranslation(["views/events", "components/dialog"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const isAdmin = useIsAdmin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -723,13 +725,17 @@ export function RecordingView({
|
|||||||
setCustomShareTimestamp(initialTimestamp);
|
setCustomShareTimestamp(initialTimestamp);
|
||||||
setShareTimestampOpen(true);
|
setShareTimestampOpen(true);
|
||||||
}}
|
}}
|
||||||
onDebugReplayClick={() => {
|
onDebugReplayClick={
|
||||||
setDebugReplayRange({
|
isAdmin
|
||||||
after: timeRange.before - 60,
|
? () => {
|
||||||
before: timeRange.before,
|
setDebugReplayRange({
|
||||||
});
|
after: timeRange.before - 60,
|
||||||
setDebugReplayMode("select");
|
before: timeRange.before,
|
||||||
}}
|
});
|
||||||
|
setDebugReplayMode("select");
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onExportClick={() => {
|
onExportClick={() => {
|
||||||
const now = new Date(timeRange.before * 1000);
|
const now = new Date(timeRange.before * 1000);
|
||||||
now.setHours(now.getHours() - 1);
|
now.setHours(now.getHours() - 1);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user