mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-21 12:36:42 +03:00
Compare commits
8 Commits
4bc7462012
...
cfc220e083
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfc220e083 | ||
|
|
e20b788966 | ||
|
|
8de0b84227 | ||
|
|
4ef37df8bd | ||
|
|
8122c31575 | ||
|
|
7a7ab98888 | ||
|
|
bb31dd18a4 | ||
|
|
5d3f31175d |
@ -177,6 +177,10 @@
|
|||||||
"noCameras": {
|
"noCameras": {
|
||||||
"title": "No Cameras Configured",
|
"title": "No Cameras Configured",
|
||||||
"description": "Get started by connecting a camera to Frigate.",
|
"description": "Get started by connecting a camera to Frigate.",
|
||||||
"buttonText": "Add Camera"
|
"buttonText": "Add Camera",
|
||||||
|
"restricted": {
|
||||||
|
"title": "No Cameras Available",
|
||||||
|
"description": "You don't have permission to view any cameras in this group."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import useSWR from "swr";
|
|||||||
import { MdHome } from "react-icons/md";
|
import { MdHome } from "react-icons/md";
|
||||||
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
||||||
import { Button, buttonVariants } from "../ui/button";
|
import { Button, buttonVariants } from "../ui/button";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { LuPencil, LuPlus } from "react-icons/lu";
|
import { LuPencil, LuPlus } from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
@ -87,6 +87,8 @@ type CameraGroupSelectorProps = {
|
|||||||
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||||
const { t } = useTranslation(["components/camera"]);
|
const { t } = useTranslation(["components/camera"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const allowedCameras = useAllowedCameras();
|
||||||
|
const isCustomRole = useIsCustomRole();
|
||||||
|
|
||||||
// tooltip
|
// tooltip
|
||||||
|
|
||||||
@ -119,10 +121,22 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(config.camera_groups).sort(
|
const allGroups = Object.entries(config.camera_groups);
|
||||||
(a, b) => a[1].order - b[1].order,
|
|
||||||
|
// If custom role, filter out groups where user has no accessible cameras
|
||||||
|
if (isCustomRole) {
|
||||||
|
return allGroups
|
||||||
|
.filter(([, groupConfig]) => {
|
||||||
|
// Check if user has access to at least one camera in this group
|
||||||
|
return groupConfig.cameras.some((cameraName) =>
|
||||||
|
allowedCameras.includes(cameraName),
|
||||||
);
|
);
|
||||||
}, [config]);
|
})
|
||||||
|
.sort((a, b) => a[1].order - b[1].order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allGroups.sort((a, b) => a[1].order - b[1].order);
|
||||||
|
}, [config, allowedCameras, isCustomRole]);
|
||||||
|
|
||||||
// add group
|
// add group
|
||||||
|
|
||||||
@ -139,6 +153,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
|||||||
activeGroup={group}
|
activeGroup={group}
|
||||||
setGroup={setGroup}
|
setGroup={setGroup}
|
||||||
deleteGroup={deleteGroup}
|
deleteGroup={deleteGroup}
|
||||||
|
isCustomRole={isCustomRole}
|
||||||
/>
|
/>
|
||||||
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
|
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
|
||||||
<div
|
<div
|
||||||
@ -206,6 +221,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{!isCustomRole && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-secondary text-muted-foreground"
|
className="bg-secondary text-muted-foreground"
|
||||||
aria-label={t("group.add")}
|
aria-label={t("group.add")}
|
||||||
@ -214,6 +230,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
|||||||
>
|
>
|
||||||
<LuPlus className="size-4 text-primary" />
|
<LuPlus className="size-4 text-primary" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{isMobile && <ScrollBar orientation="horizontal" className="h-0" />}
|
{isMobile && <ScrollBar orientation="horizontal" className="h-0" />}
|
||||||
</div>
|
</div>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
@ -228,6 +245,7 @@ type NewGroupDialogProps = {
|
|||||||
activeGroup?: string;
|
activeGroup?: string;
|
||||||
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
|
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
|
||||||
deleteGroup: () => void;
|
deleteGroup: () => void;
|
||||||
|
isCustomRole?: boolean;
|
||||||
};
|
};
|
||||||
function NewGroupDialog({
|
function NewGroupDialog({
|
||||||
open,
|
open,
|
||||||
@ -236,6 +254,7 @@ function NewGroupDialog({
|
|||||||
activeGroup,
|
activeGroup,
|
||||||
setGroup,
|
setGroup,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
|
isCustomRole,
|
||||||
}: NewGroupDialogProps) {
|
}: NewGroupDialogProps) {
|
||||||
const { t } = useTranslation(["components/camera"]);
|
const { t } = useTranslation(["components/camera"]);
|
||||||
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
|
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
|
||||||
@ -261,6 +280,12 @@ function NewGroupDialog({
|
|||||||
`${activeGroup}-draggable-layout`,
|
`${activeGroup}-draggable-layout`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setEditState("none");
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
|
|
||||||
const onDeleteGroup = useCallback(
|
const onDeleteGroup = useCallback(
|
||||||
@ -349,13 +374,7 @@ function NewGroupDialog({
|
|||||||
position="top-center"
|
position="top-center"
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
/>
|
/>
|
||||||
<Overlay
|
<Overlay open={open} onOpenChange={setOpen}>
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setEditState("none");
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Content
|
<Content
|
||||||
className={cn(
|
className={cn(
|
||||||
"scrollbar-container overflow-y-auto",
|
"scrollbar-container overflow-y-auto",
|
||||||
@ -371,6 +390,7 @@ function NewGroupDialog({
|
|||||||
>
|
>
|
||||||
<Title>{t("group.label")}</Title>
|
<Title>{t("group.label")}</Title>
|
||||||
<Description className="sr-only">{t("group.edit")}</Description>
|
<Description className="sr-only">{t("group.edit")}</Description>
|
||||||
|
{!isCustomRole && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute",
|
"absolute",
|
||||||
@ -393,6 +413,7 @@ function NewGroupDialog({
|
|||||||
<LuPlus />
|
<LuPlus />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="flex flex-col gap-4 md:gap-3">
|
<div className="flex flex-col gap-4 md:gap-3">
|
||||||
{currentGroups.map((group) => (
|
{currentGroups.map((group) => (
|
||||||
@ -401,6 +422,7 @@ function NewGroupDialog({
|
|||||||
group={group}
|
group={group}
|
||||||
onDeleteGroup={() => onDeleteGroup(group[0])}
|
onDeleteGroup={() => onDeleteGroup(group[0])}
|
||||||
onEditGroup={() => onEditGroup(group)}
|
onEditGroup={() => onEditGroup(group)}
|
||||||
|
isReadOnly={isCustomRole}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -512,12 +534,14 @@ type CameraGroupRowProps = {
|
|||||||
group: [string, CameraGroupConfig];
|
group: [string, CameraGroupConfig];
|
||||||
onDeleteGroup: () => void;
|
onDeleteGroup: () => void;
|
||||||
onEditGroup: () => void;
|
onEditGroup: () => void;
|
||||||
|
isReadOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CameraGroupRow({
|
export function CameraGroupRow({
|
||||||
group,
|
group,
|
||||||
onDeleteGroup,
|
onDeleteGroup,
|
||||||
onEditGroup,
|
onEditGroup,
|
||||||
|
isReadOnly,
|
||||||
}: CameraGroupRowProps) {
|
}: CameraGroupRowProps) {
|
||||||
const { t } = useTranslation(["components/camera"]);
|
const { t } = useTranslation(["components/camera"]);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@ -564,7 +588,7 @@ export function CameraGroupRow({
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && !isReadOnly && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu modal={!isDesktop}>
|
<DropdownMenu modal={!isDesktop}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
@ -589,7 +613,7 @@ export function CameraGroupRow({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isMobile && (
|
{!isMobile && !isReadOnly && (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@ -377,7 +377,7 @@ export default function Step1NameCamera({
|
|||||||
);
|
);
|
||||||
return selectedBrand &&
|
return selectedBrand &&
|
||||||
selectedBrand.value != "other" ? (
|
selectedBrand.value != "other" ? (
|
||||||
<Popover>
|
<Popover modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@ -600,7 +600,7 @@ export default function Step3StreamConfig({
|
|||||||
<Label className="text-sm font-medium text-primary-variant">
|
<Label className="text-sm font-medium text-primary-variant">
|
||||||
{t("cameraWizard.step3.roles")}
|
{t("cameraWizard.step3.roles")}
|
||||||
</Label>
|
</Label>
|
||||||
<Popover>
|
<Popover modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
|
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
|
||||||
<LuInfo className="size-3" />
|
<LuInfo className="size-3" />
|
||||||
@ -670,7 +670,7 @@ export default function Step3StreamConfig({
|
|||||||
<Label className="text-sm font-medium text-primary-variant">
|
<Label className="text-sm font-medium text-primary-variant">
|
||||||
{t("cameraWizard.step3.featuresTitle")}
|
{t("cameraWizard.step3.featuresTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<Popover>
|
<Popover modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
|
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
|
||||||
<LuInfo className="size-3" />
|
<LuInfo className="size-3" />
|
||||||
|
|||||||
@ -93,19 +93,23 @@ function Live() {
|
|||||||
const allowedCameras = useAllowedCameras();
|
const allowedCameras = useAllowedCameras();
|
||||||
|
|
||||||
const includesBirdseye = useMemo(() => {
|
const includesBirdseye = useMemo(() => {
|
||||||
|
// Restricted users should never have access to birdseye
|
||||||
|
if (isCustomRole) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config &&
|
config &&
|
||||||
Object.keys(config.camera_groups).length &&
|
Object.keys(config.camera_groups).length &&
|
||||||
cameraGroup &&
|
cameraGroup &&
|
||||||
config.camera_groups[cameraGroup] &&
|
config.camera_groups[cameraGroup] &&
|
||||||
cameraGroup != "default" &&
|
cameraGroup != "default"
|
||||||
(!isCustomRole || "birdseye" in allowedCameras)
|
|
||||||
) {
|
) {
|
||||||
return config.camera_groups[cameraGroup].cameras.includes("birdseye");
|
return config.camera_groups[cameraGroup].cameras.includes("birdseye");
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [config, cameraGroup, allowedCameras, isCustomRole]);
|
}, [config, cameraGroup, isCustomRole]);
|
||||||
|
|
||||||
const cameras = useMemo(() => {
|
const cameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import BlurredIconButton from "@/components/button/BlurredIconButton";
|
import BlurredIconButton from "@/components/button/BlurredIconButton";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
const allModelTypes = ["objects", "states"] as const;
|
const allModelTypes = ["objects", "states"] as const;
|
||||||
type ModelType = (typeof allModelTypes)[number];
|
type ModelType = (typeof allModelTypes)[number];
|
||||||
@ -332,9 +333,7 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
|||||||
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full items-center justify-center bg-background_alt">
|
<Skeleton className="flex size-full items-center justify-center" />
|
||||||
<MdModelTraining className="size-16 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
|
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
|
||||||
{config.name}
|
{config.name}
|
||||||
|
|||||||
@ -20,7 +20,14 @@ import {
|
|||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
} from "@/types/frigateConfig";
|
} from "@/types/frigateConfig";
|
||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
isDesktop,
|
isDesktop,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -46,6 +53,8 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { EmptyCard } from "@/components/card/EmptyCard";
|
import { EmptyCard } from "@/components/card/EmptyCard";
|
||||||
import { BsFillCameraVideoOffFill } from "react-icons/bs";
|
import { BsFillCameraVideoOffFill } from "react-icons/bs";
|
||||||
|
import { AuthContext } from "@/context/auth-context";
|
||||||
|
import { useIsCustomRole } from "@/hooks/use-is-custom-role";
|
||||||
|
|
||||||
type LiveDashboardViewProps = {
|
type LiveDashboardViewProps = {
|
||||||
cameras: CameraConfig[];
|
cameras: CameraConfig[];
|
||||||
@ -374,10 +383,6 @@ export default function LiveDashboardView({
|
|||||||
onSaveMuting(true);
|
onSaveMuting(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cameras.length == 0 && !includeBirdseye) {
|
|
||||||
return <NoCameraView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="scrollbar-container size-full select-none overflow-y-auto px-1 pt-2 md:p-2"
|
className="scrollbar-container size-full select-none overflow-y-auto px-1 pt-2 md:p-2"
|
||||||
@ -439,6 +444,10 @@ export default function LiveDashboardView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{cameras.length == 0 && !includeBirdseye ? (
|
||||||
|
<NoCameraView />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{!fullscreen && events && events.length > 0 && (
|
{!fullscreen && events && events.length > 0 && (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -494,7 +503,8 @@ export default function LiveDashboardView({
|
|||||||
)}
|
)}
|
||||||
{cameras.map((camera) => {
|
{cameras.map((camera) => {
|
||||||
let grow;
|
let grow;
|
||||||
const aspectRatio = camera.detect.width / camera.detect.height;
|
const aspectRatio =
|
||||||
|
camera.detect.width / camera.detect.height;
|
||||||
if (aspectRatio > 2) {
|
if (aspectRatio > 2) {
|
||||||
grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
||||||
} else if (aspectRatio < 1) {
|
} else if (aspectRatio < 1) {
|
||||||
@ -503,10 +513,12 @@ export default function LiveDashboardView({
|
|||||||
grow = "aspect-video";
|
grow = "aspect-video";
|
||||||
}
|
}
|
||||||
const availableStreams = camera.live.streams || {};
|
const availableStreams = camera.live.streams || {};
|
||||||
const firstStreamEntry = Object.values(availableStreams)[0] || "";
|
const firstStreamEntry =
|
||||||
|
Object.values(availableStreams)[0] || "";
|
||||||
|
|
||||||
const streamNameFromSettings =
|
const streamNameFromSettings =
|
||||||
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
currentGroupStreamingSettings?.[camera.name]?.streamName ||
|
||||||
|
"";
|
||||||
const streamExists =
|
const streamExists =
|
||||||
streamNameFromSettings &&
|
streamNameFromSettings &&
|
||||||
Object.values(availableStreams).includes(
|
Object.values(availableStreams).includes(
|
||||||
@ -535,7 +547,9 @@ export default function LiveDashboardView({
|
|||||||
camera={camera.name}
|
camera={camera.name}
|
||||||
cameraGroup={cameraGroup}
|
cameraGroup={cameraGroup}
|
||||||
streamName={streamName}
|
streamName={streamName}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={
|
||||||
|
preferredLiveModes[camera.name] ?? "mse"
|
||||||
|
}
|
||||||
isRestreamed={isRestreamedStates[camera.name]}
|
isRestreamed={isRestreamedStates[camera.name]}
|
||||||
supportsAudio={
|
supportsAudio={
|
||||||
supportsAudioOutputStates[streamName]?.supportsAudio ??
|
supportsAudioOutputStates[streamName]?.supportsAudio ??
|
||||||
@ -566,9 +580,13 @@ export default function LiveDashboardView({
|
|||||||
windowVisible && visibleCameras.includes(camera.name)
|
windowVisible && visibleCameras.includes(camera.name)
|
||||||
}
|
}
|
||||||
cameraConfig={camera}
|
cameraConfig={camera}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={
|
||||||
|
preferredLiveModes[camera.name] ?? "mse"
|
||||||
|
}
|
||||||
autoLive={autoLive ?? globalAutoLive}
|
autoLive={autoLive ?? globalAutoLive}
|
||||||
showStillWithoutActivity={showStillWithoutActivity ?? true}
|
showStillWithoutActivity={
|
||||||
|
showStillWithoutActivity ?? true
|
||||||
|
}
|
||||||
alwaysShowCameraName={displayCameraNames}
|
alwaysShowCameraName={displayCameraNames}
|
||||||
useWebGL={useWebGL}
|
useWebGL={useWebGL}
|
||||||
playInBackground={false}
|
playInBackground={false}
|
||||||
@ -576,7 +594,9 @@ export default function LiveDashboardView({
|
|||||||
streamName={streamName}
|
streamName={streamName}
|
||||||
onClick={() => onSelectCamera(camera.name)}
|
onClick={() => onSelectCamera(camera.name)}
|
||||||
onError={(e) => handleError(camera.name, e)}
|
onError={(e) => handleError(camera.name, e)}
|
||||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
onResetLiveMode={() =>
|
||||||
|
resetPreferredLiveMode(camera.name)
|
||||||
|
}
|
||||||
playAudio={audioStates[camera.name] ?? false}
|
playAudio={audioStates[camera.name] ?? false}
|
||||||
volume={volumeStates[camera.name]}
|
volume={volumeStates[camera.name]}
|
||||||
/>
|
/>
|
||||||
@ -632,21 +652,34 @@ export default function LiveDashboardView({
|
|||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoCameraView() {
|
function NoCameraView() {
|
||||||
const { t } = useTranslation(["views/live"]);
|
const { t } = useTranslation(["views/live"]);
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const isCustomRole = useIsCustomRole();
|
||||||
|
|
||||||
|
// Check if this is a restricted user with no cameras in this group
|
||||||
|
const isRestricted = isCustomRole && auth.isAuthenticated;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center justify-center">
|
<div className="flex size-full items-center justify-center">
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
icon={<BsFillCameraVideoOffFill className="size-8" />}
|
icon={<BsFillCameraVideoOffFill className="size-8" />}
|
||||||
title={t("noCameras.title")}
|
title={
|
||||||
description={t("noCameras.description")}
|
isRestricted ? t("noCameras.restricted.title") : t("noCameras.title")
|
||||||
buttonText={t("noCameras.buttonText")}
|
}
|
||||||
link="/settings?page=cameraManagement"
|
description={
|
||||||
|
isRestricted
|
||||||
|
? t("noCameras.restricted.description")
|
||||||
|
: t("noCameras.description")
|
||||||
|
}
|
||||||
|
buttonText={!isRestricted ? t("noCameras.buttonText") : undefined}
|
||||||
|
link={!isRestricted ? "/settings?page=cameraManagement" : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -729,12 +729,9 @@ export default function GeneralMetrics({
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className="aspect-video w-full" />
|
<Skeleton className="aspect-video w-full" />
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{statsHistory[0]?.npu_usages && (
|
{statsHistory[0]?.npu_usages && (
|
||||||
<div
|
<>
|
||||||
className={cn("mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2")}
|
|
||||||
>
|
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
@ -755,7 +752,9 @@ export default function GeneralMetrics({
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton className="aspect-video w-full" />
|
<Skeleton className="aspect-video w-full" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user