From 7a7ab98888d5847c5d5ada27ef54a07f62f94045 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:39:36 -0600 Subject: [PATCH] always show camera group buttons on mobile so users don't get stuck --- web/public/locales/en/views/live.json | 6 +- web/src/views/live/LiveDashboardView.tsx | 419 ++++++++++++----------- 2 files changed, 231 insertions(+), 194 deletions(-) diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index 085aa0a49..21f367ea9 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -177,6 +177,10 @@ "noCameras": { "title": "No Cameras Configured", "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." + } } } diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index c4104576c..c096e05ef 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -20,7 +20,14 @@ import { FrigateConfig, } from "@/types/frigateConfig"; import { ReviewSegment } from "@/types/review"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { isDesktop, isMobile, @@ -46,6 +53,8 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider"; import { useTranslation } from "react-i18next"; import { EmptyCard } from "@/components/card/EmptyCard"; import { BsFillCameraVideoOffFill } from "react-icons/bs"; +import { AuthContext } from "@/context/auth-context"; +import { useIsCustomRole } from "@/hooks/use-is-custom-role"; type LiveDashboardViewProps = { cameras: CameraConfig[]; @@ -374,10 +383,6 @@ export default function LiveDashboardView({ onSaveMuting(true); }; - if (cameras.length == 0 && !includeBirdseye) { - return ; - } - return (
)} - {!fullscreen && events && events.length > 0 && ( - - -
- {events.map((event) => { - return ( - - ); - })} -
-
- -
- )} - - {!cameraGroup || cameraGroup == "default" || isMobileOnly ? ( + {cameras.length == 0 && !includeBirdseye ? ( + + ) : ( <> -
- {includeBirdseye && birdseyeConfig?.enabled && ( + {!fullscreen && events && events.length > 0 && ( + + +
+ {events.map((event) => { + return ( + + ); + })} +
+
+ +
+ )} + + {!cameraGroup || cameraGroup == "default" || isMobileOnly ? ( + <>
{ - const aspectRatio = - birdseyeConfig.width / birdseyeConfig.height; - if (aspectRatio > 2) { - return `${mobileLayout == "grid" && "col-span-2"} aspect-wide`; - } else if (aspectRatio < 1) { - return `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`; - } else { - return "aspect-video"; - } - })()} - ref={birdseyeContainerRef} + className={cn( + "mt-2 grid grid-cols-1 gap-2 px-2 md:gap-4", + mobileLayout == "grid" && + "grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4", + isMobile && "px-0", + )} > - onSelectCamera("birdseye")} - containerRef={birdseyeContainerRef} - /> -
- )} - {cameras.map((camera) => { - let grow; - const aspectRatio = camera.detect.width / camera.detect.height; - if (aspectRatio > 2) { - grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`; - } else if (aspectRatio < 1) { - grow = `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`; - } else { - grow = "aspect-video"; - } - const availableStreams = camera.live.streams || {}; - const firstStreamEntry = Object.values(availableStreams)[0] || ""; - - const streamNameFromSettings = - currentGroupStreamingSettings?.[camera.name]?.streamName || ""; - const streamExists = - streamNameFromSettings && - Object.values(availableStreams).includes( - streamNameFromSettings, - ); - - const streamName = streamExists - ? streamNameFromSettings - : firstStreamEntry; - const streamType = - currentGroupStreamingSettings?.[camera.name]?.streamType; - const autoLive = - streamType !== undefined - ? streamType !== "no-streaming" - : undefined; - const showStillWithoutActivity = - currentGroupStreamingSettings?.[camera.name]?.streamType !== - "continuous"; - const useWebGL = - currentGroupStreamingSettings?.[camera.name] - ?.compatibilityMode || false; - return ( - toggleAudio(camera.name)} - statsState={statsStates[camera.name]} - toggleStats={() => toggleStats(camera.name)} - volumeState={volumeStates[camera.name] ?? 1} - setVolumeState={(value) => - setVolumeStates({ - [camera.name]: value, - }) - } - muteAll={muteAll} - unmuteAll={unmuteAll} - resetPreferredLiveMode={() => - resetPreferredLiveMode(camera.name) - } - config={config} - > - onSelectCamera(camera.name)} - onError={(e) => handleError(camera.name, e)} - onResetLiveMode={() => resetPreferredLiveMode(camera.name)} - playAudio={audioStates[camera.name] ?? false} - volume={volumeStates[camera.name]} - /> - - ); - })} -
- {isDesktop && ( -
- - + {includeBirdseye && birdseyeConfig?.enabled && (
{ + const aspectRatio = + birdseyeConfig.width / birdseyeConfig.height; + if (aspectRatio > 2) { + return `${mobileLayout == "grid" && "col-span-2"} aspect-wide`; + } else if (aspectRatio < 1) { + return `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`; + } else { + return "aspect-video"; + } + })()} + ref={birdseyeContainerRef} > - {fullscreen ? ( - - ) : ( - - )} + onSelectCamera("birdseye")} + containerRef={birdseyeContainerRef} + />
-
- - {fullscreen - ? t("button.exitFullscreen", { ns: "common" }) - : t("button.fullscreen", { ns: "common" })} - -
-
+ )} + {cameras.map((camera) => { + let grow; + const aspectRatio = + camera.detect.width / camera.detect.height; + if (aspectRatio > 2) { + grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`; + } else if (aspectRatio < 1) { + grow = `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`; + } else { + grow = "aspect-video"; + } + const availableStreams = camera.live.streams || {}; + const firstStreamEntry = + Object.values(availableStreams)[0] || ""; + + const streamNameFromSettings = + currentGroupStreamingSettings?.[camera.name]?.streamName || + ""; + const streamExists = + streamNameFromSettings && + Object.values(availableStreams).includes( + streamNameFromSettings, + ); + + const streamName = streamExists + ? streamNameFromSettings + : firstStreamEntry; + const streamType = + currentGroupStreamingSettings?.[camera.name]?.streamType; + const autoLive = + streamType !== undefined + ? streamType !== "no-streaming" + : undefined; + const showStillWithoutActivity = + currentGroupStreamingSettings?.[camera.name]?.streamType !== + "continuous"; + const useWebGL = + currentGroupStreamingSettings?.[camera.name] + ?.compatibilityMode || false; + return ( + toggleAudio(camera.name)} + statsState={statsStates[camera.name]} + toggleStats={() => toggleStats(camera.name)} + volumeState={volumeStates[camera.name] ?? 1} + setVolumeState={(value) => + setVolumeStates({ + [camera.name]: value, + }) + } + muteAll={muteAll} + unmuteAll={unmuteAll} + resetPreferredLiveMode={() => + resetPreferredLiveMode(camera.name) + } + config={config} + > + onSelectCamera(camera.name)} + onError={(e) => handleError(camera.name, e)} + onResetLiveMode={() => + resetPreferredLiveMode(camera.name) + } + playAudio={audioStates[camera.name] ?? false} + volume={volumeStates[camera.name]} + /> + + ); + })} +
+ {isDesktop && ( +
+ + +
+ {fullscreen ? ( + + ) : ( + + )} +
+
+ + {fullscreen + ? t("button.exitFullscreen", { ns: "common" }) + : t("button.fullscreen", { ns: "common" })} + +
+
+ )} + + ) : ( + )} - ) : ( - )} ); @@ -638,15 +660,26 @@ export default function LiveDashboardView({ function NoCameraView() { 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 (
} - title={t("noCameras.title")} - description={t("noCameras.description")} - buttonText={t("noCameras.buttonText")} - link="/settings?page=cameraManagement" + title={ + isRestricted ? t("noCameras.restricted.title") : t("noCameras.title") + } + description={ + isRestricted + ? t("noCameras.restricted.description") + : t("noCameras.description") + } + buttonText={!isRestricted ? t("noCameras.buttonText") : undefined} + link={!isRestricted ? "/settings?page=cameraManagement" : undefined} />
);