make live view settings drawer scrollable

This commit is contained in:
Josh Hawkins 2025-11-23 12:25:23 -06:00
parent 9242997079
commit bf4f63e50e

View File

@ -1376,329 +1376,343 @@ function FrigateCameraFeatures({
title={t("cameraSettings.title", { camera })} title={t("cameraSettings.title", { camera })}
/> />
</DrawerTrigger> </DrawerTrigger>
<DrawerContent className="rounded-2xl px-2 py-4"> <DrawerContent className="max-h-[75dvh] overflow-hidden rounded-2xl">
<div className="mt-2 flex flex-col gap-2"> <div className="scrollbar-container mt-2 flex h-auto flex-col gap-2 overflow-y-auto px-2 py-4">
{isAdmin && ( <>
<> {isAdmin && (
<FilterSwitch <>
label={t("cameraSettings.cameraEnabled")}
isChecked={enabledState == "ON"}
onCheckedChange={() =>
sendEnabled(enabledState == "ON" ? "OFF" : "ON")
}
/>
<FilterSwitch
label={t("cameraSettings.objectDetection")}
isChecked={detectState == "ON"}
onCheckedChange={() =>
sendDetect(detectState == "ON" ? "OFF" : "ON")
}
/>
{recordingEnabled && (
<FilterSwitch <FilterSwitch
label={t("cameraSettings.recording")} label={t("cameraSettings.cameraEnabled")}
isChecked={recordState == "ON"} isChecked={enabledState == "ON"}
onCheckedChange={() => onCheckedChange={() =>
sendRecord(recordState == "ON" ? "OFF" : "ON") sendEnabled(enabledState == "ON" ? "OFF" : "ON")
} }
/> />
)}
<FilterSwitch
label={t("cameraSettings.snapshots")}
isChecked={snapshotState == "ON"}
onCheckedChange={() =>
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
}
/>
{audioDetectEnabled && (
<FilterSwitch <FilterSwitch
label={t("cameraSettings.audioDetection")} label={t("cameraSettings.objectDetection")}
isChecked={audioState == "ON"} isChecked={detectState == "ON"}
onCheckedChange={() => onCheckedChange={() =>
sendAudio(audioState == "ON" ? "OFF" : "ON") sendDetect(detectState == "ON" ? "OFF" : "ON")
} }
/> />
)} {recordingEnabled && (
{audioDetectEnabled && transcriptionEnabled && ( <FilterSwitch
label={t("cameraSettings.recording")}
isChecked={recordState == "ON"}
onCheckedChange={() =>
sendRecord(recordState == "ON" ? "OFF" : "ON")
}
/>
)}
<FilterSwitch <FilterSwitch
label={t("cameraSettings.transcription")} label={t("cameraSettings.snapshots")}
disabled={audioState == "OFF"} isChecked={snapshotState == "ON"}
isChecked={transcriptionState == "ON"}
onCheckedChange={() => onCheckedChange={() =>
sendTranscription(transcriptionState == "ON" ? "OFF" : "ON") sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
} }
/> />
)} {audioDetectEnabled && (
{autotrackingEnabled && ( <FilterSwitch
<FilterSwitch label={t("cameraSettings.audioDetection")}
label={t("cameraSettings.autotracking")} isChecked={audioState == "ON"}
isChecked={autotrackingState == "ON"} onCheckedChange={() =>
onCheckedChange={() => sendAudio(audioState == "ON" ? "OFF" : "ON")
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON") }
} />
/> )}
)} {audioDetectEnabled && transcriptionEnabled && (
</> <FilterSwitch
)} label={t("cameraSettings.transcription")}
</div> disabled={audioState == "OFF"}
isChecked={transcriptionState == "ON"}
onCheckedChange={() =>
sendTranscription(
transcriptionState == "ON" ? "OFF" : "ON",
)
}
/>
)}
{autotrackingEnabled && (
<FilterSwitch
label={t("cameraSettings.autotracking")}
isChecked={autotrackingState == "ON"}
onCheckedChange={() =>
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
}
/>
)}
</>
)}
<div className="mt-3 flex flex-col gap-5"> <div className="mt-3 flex flex-col gap-5">
{!isRestreamed && ( {!isRestreamed && (
<div className="flex flex-col gap-2 p-2"> <div className="flex flex-col gap-2 p-2">
<Label>{t("stream.title")}</Label> <Label>{t("stream.title")}</Label>
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<LuX className="size-4 text-danger" />
<div>
{t("streaming.restreaming.disabled", {
ns: "components/dialog",
})}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80 text-xs">
{t("streaming.restreaming.desc.title", {
ns: "components/dialog",
})}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl("configuration/live")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</div>
</div>
)}
{isRestreamed && Object.values(camera.live.streams).length > 0 && (
<div className="mt-1 p-2">
<div className="mb-1 text-sm">{t("stream.title")}</div>
<Select
value={streamName}
onValueChange={(value) => {
setStreamName?.(value);
}}
disabled={debug}
>
<SelectTrigger className="w-full">
<SelectValue>
{Object.keys(camera.live.streams).find(
(key) => camera.live.streams[key] === streamName,
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(camera.live.streams).map(
([stream, name]) => (
<SelectItem
key={stream}
className="cursor-pointer"
value={name}
>
{stream}
</SelectItem>
),
)}
</SelectGroup>
</SelectContent>
</Select>
{debug && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<>
<LuX className="size-8 text-danger" />
<div>{t("stream.debug.picker")}</div>
</>
</div>
)}
{preferredLiveMode != "jsmpeg" && !debug && isRestreamed && (
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supportsAudioOutput ? (
<>
<LuCheck className="size-4 text-success" />
<div>{t("stream.audio.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>{t("stream.audio.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
{t("stream.audio.tips.title")}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl("configuration/live")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</>
)}
</div>
)}
{preferredLiveMode != "jsmpeg" &&
!debug &&
isRestreamed &&
supportsAudioOutput && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supports2WayTalk ? ( <LuX className="size-4 text-danger" />
<> <div>
<LuCheck className="size-4 text-success" /> {t("streaming.restreaming.disabled", {
<div>{t("stream.twoWayTalk.available")}</div> ns: "components/dialog",
</> })}
) : ( </div>
<> <Popover>
<LuX className="size-4 text-danger" /> <PopoverTrigger asChild>
<div>{t("stream.twoWayTalk.unavailable")}</div> <div className="cursor-pointer p-0">
<Popover> <LuInfo className="size-4" />
<PopoverTrigger asChild> <span className="sr-only">
<div className="cursor-pointer p-0"> {t("button.info", { ns: "common" })}
<LuInfo className="size-4" /> </span>
<span className="sr-only"> </div>
{t("button.info", { ns: "common" })} </PopoverTrigger>
</span> <PopoverContent className="w-80 text-xs">
</div> {t("streaming.restreaming.desc.title", {
</PopoverTrigger> ns: "components/dialog",
<PopoverContent className="w-52 text-xs"> })}
{t("stream.twoWayTalk.tips")} <div className="mt-2 flex items-center text-primary">
<div className="mt-2 flex items-center text-primary"> <Link
<Link to={getLocaleDocUrl("configuration/live")}
to={getLocaleDocUrl( target="_blank"
"configuration/live/#webrtc-extra-configuration", rel="noopener noreferrer"
)} className="inline"
target="_blank" >
rel="noopener noreferrer" {t("readTheDocumentation", { ns: "common" })}
className="inline" <LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</div>
</div>
)}
{isRestreamed &&
Object.values(camera.live.streams).length > 0 && (
<div className="mt-1 p-2">
<div className="mb-1 text-sm">{t("stream.title")}</div>
<Select
value={streamName}
onValueChange={(value) => {
setStreamName?.(value);
}}
disabled={debug}
>
<SelectTrigger className="w-full">
<SelectValue>
{Object.keys(camera.live.streams).find(
(key) => camera.live.streams[key] === streamName,
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(camera.live.streams).map(
([stream, name]) => (
<SelectItem
key={stream}
className="cursor-pointer"
value={name}
> >
{t("readTheDocumentation", { ns: "common" })} {stream}
<LuExternalLink className="ml-2 inline-flex size-3" /> </SelectItem>
</Link> ),
</div> )}
</PopoverContent> </SelectGroup>
</Popover> </SelectContent>
</> </Select>
{debug && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<>
<LuX className="size-8 text-danger" />
<div>{t("stream.debug.picker")}</div>
</>
</div>
)}
{preferredLiveMode != "jsmpeg" &&
!debug &&
isRestreamed && (
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supportsAudioOutput ? (
<>
<LuCheck className="size-4 text-success" />
<div>{t("stream.audio.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>{t("stream.audio.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
{t("stream.audio.tips.title")}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl("configuration/live")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", {
ns: "common",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</>
)}
</div>
)}
{preferredLiveMode != "jsmpeg" &&
!debug &&
isRestreamed &&
supportsAudioOutput && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supports2WayTalk ? (
<>
<LuCheck className="size-4 text-success" />
<div>{t("stream.twoWayTalk.available")}</div>
</>
) : (
<>
<LuX className="size-4 text-danger" />
<div>{t("stream.twoWayTalk.unavailable")}</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">
{t("button.info", { ns: "common" })}
</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-52 text-xs">
{t("stream.twoWayTalk.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl(
"configuration/live/#webrtc-extra-configuration",
)}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", {
ns: "common",
})}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</PopoverContent>
</Popover>
</>
)}
</div>
)}
{preferredLiveMode == "jsmpeg" && isRestreamed && (
<div className="mt-2 flex flex-col items-center gap-3">
<div className="flex flex-row items-center gap-2">
<IoIosWarning className="mr-1 size-8 text-danger" />
<p className="text-sm">
{t("stream.lowBandwidth.tips")}
</p>
</div>
<Button
className={`flex items-center gap-2.5 rounded-lg`}
aria-label={t("stream.lowBandwidth.resetStream")}
variant="outline"
size="sm"
disabled={debug}
onClick={() => setLowBandwidth(false)}
>
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
<div className="text-primary-variant">
{t("stream.lowBandwidth.resetStream")}
</div>
</Button>
</div>
)} )}
</div> </div>
)} )}
{preferredLiveMode == "jsmpeg" && isRestreamed && ( <div className="flex flex-col gap-1 px-2">
<div className="mt-2 flex flex-col items-center gap-3"> <div className="mb-1 text-sm font-medium leading-none">
<div className="flex flex-row items-center gap-2"> {t("manualRecording.title")}
<IoIosWarning className="mr-1 size-8 text-danger" /> </div>
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p> <div className="flex flex-row items-stretch gap-2">
</div>
<Button <Button
className={`flex items-center gap-2.5 rounded-lg`} onClick={handleSnapshotClick}
aria-label={t("stream.lowBandwidth.resetStream")} disabled={!cameraEnabled || debug || isSnapshotLoading}
variant="outline" className="h-auto w-full whitespace-normal"
size="sm"
disabled={debug}
onClick={() => setLowBandwidth(false)}
> >
<MdOutlineRestartAlt className="size-5 text-primary-variant" /> {isSnapshotLoading && (
<div className="text-primary-variant"> <ActivityIndicator className="mr-2 size-4" />
{t("stream.lowBandwidth.resetStream")} )}
</div> {t("snapshot.takeSnapshot")}
</Button>
<Button
onClick={handleEventButtonClick}
className={cn(
"h-auto w-full whitespace-normal",
isRecording &&
"animate-pulse bg-red-500 hover:bg-red-600",
)}
disabled={debug}
>
{t("manualRecording." + (isRecording ? "end" : "start"))}
</Button> </Button>
</div> </div>
<p className="text-sm text-muted-foreground">
{t("manualRecording.tips")}
</p>
</div>
{isRestreamed && (
<>
<div className="flex flex-col gap-2">
<FilterSwitch
label={t("manualRecording.playInBackground.label")}
isChecked={playInBackground}
onCheckedChange={(checked) => {
setPlayInBackground(checked);
}}
disabled={debug}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.playInBackground.desc")}
</p>
</div>
<div className="flex flex-col gap-2">
<FilterSwitch
label={t("manualRecording.showStats.label")}
isChecked={showStats}
onCheckedChange={(checked) => {
setShowStats(checked);
}}
disabled={debug}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.showStats.desc")}
</p>
</div>
</>
)} )}
</div> <div className="mb-3 flex flex-col">
)}
<div className="flex flex-col gap-1 px-2">
<div className="mb-1 text-sm font-medium leading-none">
{t("manualRecording.title")}
</div>
<div className="flex flex-row items-stretch gap-2">
<Button
onClick={handleSnapshotClick}
disabled={!cameraEnabled || debug || isSnapshotLoading}
className="h-auto w-full whitespace-normal"
>
{isSnapshotLoading && (
<ActivityIndicator className="mr-2 size-4" />
)}
{t("snapshot.takeSnapshot")}
</Button>
<Button
onClick={handleEventButtonClick}
className={cn(
"h-auto w-full whitespace-normal",
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
)}
disabled={debug}
>
{t("manualRecording." + (isRecording ? "end" : "start"))}
</Button>
</div>
<p className="text-sm text-muted-foreground">
{t("manualRecording.tips")}
</p>
</div>
{isRestreamed && (
<>
<div className="flex flex-col gap-2">
<FilterSwitch <FilterSwitch
label={t("manualRecording.playInBackground.label")} label={t("streaming.debugView", { ns: "components/dialog" })}
isChecked={playInBackground} isChecked={debug}
onCheckedChange={(checked) => { onCheckedChange={(checked) => setDebug(checked)}
setPlayInBackground(checked);
}}
disabled={debug}
/> />
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.playInBackground.desc")}
</p>
</div> </div>
<div className="flex flex-col gap-2"> </div>
<FilterSwitch </>
label={t("manualRecording.showStats.label")}
isChecked={showStats}
onCheckedChange={(checked) => {
setShowStats(checked);
}}
disabled={debug}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.showStats.desc")}
</p>
</div>
</>
)}
<div className="mb-3 flex flex-col">
<FilterSwitch
label={t("streaming.debugView", { ns: "components/dialog" })}
isChecked={debug}
onCheckedChange={(checked) => setDebug(checked)}
/>
</div>
</div> </div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>