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": {
"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."
}
}
}

View File

@ -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 <NoCameraView />;
}
return (
<div
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>
)}
{cameras.length == 0 && !includeBirdseye ? (
<NoCameraView />
) : (
<>
{!fullscreen && events && events.length > 0 && (
<ScrollArea>
<TooltipProvider>
@ -494,7 +503,8 @@ export default function LiveDashboardView({
)}
{cameras.map((camera) => {
let grow;
const aspectRatio = camera.detect.width / camera.detect.height;
const aspectRatio =
camera.detect.width / camera.detect.height;
if (aspectRatio > 2) {
grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
} else if (aspectRatio < 1) {
@ -503,10 +513,12 @@ export default function LiveDashboardView({
grow = "aspect-video";
}
const availableStreams = camera.live.streams || {};
const firstStreamEntry = Object.values(availableStreams)[0] || "";
const firstStreamEntry =
Object.values(availableStreams)[0] || "";
const streamNameFromSettings =
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
currentGroupStreamingSettings?.[camera.name]?.streamName ||
"";
const streamExists =
streamNameFromSettings &&
Object.values(availableStreams).includes(
@ -535,7 +547,9 @@ export default function LiveDashboardView({
camera={camera.name}
cameraGroup={cameraGroup}
streamName={streamName}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
preferredLiveMode={
preferredLiveModes[camera.name] ?? "mse"
}
isRestreamed={isRestreamedStates[camera.name]}
supportsAudio={
supportsAudioOutputStates[streamName]?.supportsAudio ??
@ -566,9 +580,13 @@ export default function LiveDashboardView({
windowVisible && visibleCameras.includes(camera.name)
}
cameraConfig={camera}
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
preferredLiveMode={
preferredLiveModes[camera.name] ?? "mse"
}
autoLive={autoLive ?? globalAutoLive}
showStillWithoutActivity={showStillWithoutActivity ?? true}
showStillWithoutActivity={
showStillWithoutActivity ?? true
}
alwaysShowCameraName={displayCameraNames}
useWebGL={useWebGL}
playInBackground={false}
@ -576,7 +594,9 @@ export default function LiveDashboardView({
streamName={streamName}
onClick={() => onSelectCamera(camera.name)}
onError={(e) => handleError(camera.name, e)}
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
onResetLiveMode={() =>
resetPreferredLiveMode(camera.name)
}
playAudio={audioStates[camera.name] ?? false}
volume={volumeStates[camera.name]}
/>
@ -632,21 +652,34 @@ export default function LiveDashboardView({
toggleFullscreen={toggleFullscreen}
/>
)}
</>
)}
</div>
);
}
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 (
<div className="flex size-full items-center justify-center">
<EmptyCard
icon={<BsFillCameraVideoOffFill className="size-8" />}
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}
/>
</div>
);