always show camera group buttons on mobile so users don't get stuck

This commit is contained in:
Josh Hawkins 2025-11-20 16:39:36 -06:00
parent bb31dd18a4
commit 7a7ab98888
2 changed files with 231 additions and 194 deletions

View File

@ -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."
}
} }
} }

View File

@ -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>
); );