mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 00:22:19 +03:00
feat: add i18n (translation/localization) (#16877)
* Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
co-authored by
Josh Hawkins
parent
db541abed4
commit
d34533981f
@@ -50,6 +50,7 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
||||
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type DraggableGridLayoutProps = {
|
||||
cameras: CameraConfig[];
|
||||
@@ -79,6 +80,7 @@ export default function DraggableGridLayout({
|
||||
fullscreen,
|
||||
toggleFullscreen,
|
||||
}: DraggableGridLayoutProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||
|
||||
@@ -658,7 +660,9 @@ export default function DraggableGridLayout({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isEditMode ? "Exit Editing" : "Edit Layout"}
|
||||
{isEditMode
|
||||
? t("editLayout.exitEdit")
|
||||
: t("editLayout.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!isEditMode && (
|
||||
@@ -676,7 +680,9 @@ export default function DraggableGridLayout({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isEditMode ? "Exit Editing" : "Edit Camera Group"}
|
||||
{isEditMode
|
||||
? t("editLayout.exitEdit")
|
||||
: t("editLayout.group.label")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -694,7 +700,9 @@ export default function DraggableGridLayout({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{fullscreen ? "Exit Fullscreen" : "Fullscreen"}
|
||||
{fullscreen
|
||||
? t("button.exitFullscreen", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isSafari,
|
||||
useMobileOrientation,
|
||||
} from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaCompress, FaExpand } from "react-icons/fa";
|
||||
import { IoMdArrowBack } from "react-icons/io";
|
||||
import { LuPictureInPicture } from "react-icons/lu";
|
||||
@@ -32,6 +33,7 @@ export default function LiveBirdseyeView({
|
||||
fullscreen,
|
||||
toggleFullscreen,
|
||||
}: LiveBirdseyeViewProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const { isPortrait } = useMobileOrientation();
|
||||
@@ -144,12 +146,16 @@ export default function LiveBirdseyeView({
|
||||
{!fullscreen ? (
|
||||
<Button
|
||||
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
|
||||
aria-label="Go Back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size={isMobile ? "icon" : "sm"}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowBack className="size-5" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
@@ -164,7 +170,11 @@ export default function LiveBirdseyeView({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={fullscreen ? FaCompress : FaExpand}
|
||||
isActive={fullscreen}
|
||||
title={fullscreen ? "Close" : "Fullscreen"}
|
||||
title={
|
||||
fullscreen
|
||||
? t("button.close", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })
|
||||
}
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
)}
|
||||
@@ -174,7 +184,11 @@ export default function LiveBirdseyeView({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={LuPictureInPicture}
|
||||
isActive={pip}
|
||||
title={pip ? "Close" : "Picture in Picture"}
|
||||
title={
|
||||
pip
|
||||
? t("button.close", { ns: "common" })
|
||||
: t("button.pictureInPicture", { ns: "common" })
|
||||
}
|
||||
onClick={() => {
|
||||
if (!pip) {
|
||||
setPip(true);
|
||||
|
||||
@@ -103,6 +103,7 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||
import useSWR from "swr";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSessionPersistence } from "@/hooks/use-session-persistence";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -117,6 +118,7 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type LiveCameraViewProps = {
|
||||
config?: FrigateConfig;
|
||||
@@ -132,6 +134,7 @@ export default function LiveCameraView({
|
||||
fullscreen,
|
||||
toggleFullscreen,
|
||||
}: LiveCameraViewProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const navigate = useNavigate();
|
||||
const { isPortrait } = useMobileOrientation();
|
||||
const mainRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -427,16 +430,20 @@ export default function LiveCameraView({
|
||||
>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Show historical footage"
|
||||
aria-label={t("history.label")}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate("review", {
|
||||
@@ -452,7 +459,11 @@ export default function LiveCameraView({
|
||||
}}
|
||||
>
|
||||
<LuHistory className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">History</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("button.history", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -465,13 +476,15 @@ export default function LiveCameraView({
|
||||
{fullscreen && (
|
||||
<Button
|
||||
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-secondary-foreground">Back</div>
|
||||
<div className="text-secondary-foreground">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -481,7 +494,11 @@ export default function LiveCameraView({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={fullscreen ? FaCompress : FaExpand}
|
||||
isActive={fullscreen}
|
||||
title={fullscreen ? "Close" : "Fullscreen"}
|
||||
title={
|
||||
fullscreen
|
||||
? t("button.close", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })
|
||||
}
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
)}
|
||||
@@ -491,7 +508,11 @@ export default function LiveCameraView({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={LuPictureInPicture}
|
||||
isActive={pip}
|
||||
title={pip ? "Close" : "Picture in Picture"}
|
||||
title={
|
||||
pip
|
||||
? t("button.close", { ns: "common" })
|
||||
: t("button.pictureInPicture", { ns: "common" })
|
||||
}
|
||||
onClick={() => {
|
||||
if (!pip) {
|
||||
setPip(true);
|
||||
@@ -667,6 +688,7 @@ function PtzControlPanel({
|
||||
clickOverlay: boolean;
|
||||
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
|
||||
|
||||
const { send: sendPtz } = usePtzCommand(camera);
|
||||
@@ -752,7 +774,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("pt") && (
|
||||
<>
|
||||
<TooltipButton
|
||||
label="Move camera left"
|
||||
label={t("ptz.move.left.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_LEFT");
|
||||
@@ -767,7 +789,7 @@ function PtzControlPanel({
|
||||
<FaAngleLeft />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label="Move camera up"
|
||||
label={t("ptz.move.up.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_UP");
|
||||
@@ -782,7 +804,7 @@ function PtzControlPanel({
|
||||
<FaAngleUp />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label="Move camera down"
|
||||
label={t("ptz.move.down.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_DOWN");
|
||||
@@ -797,7 +819,7 @@ function PtzControlPanel({
|
||||
<FaAngleDown />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label="Move camera right"
|
||||
label={t("ptz.move.right.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_RIGHT");
|
||||
@@ -816,7 +838,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("zoom") && (
|
||||
<>
|
||||
<TooltipButton
|
||||
label="Zoom in"
|
||||
label={t("ptz.zoom.in.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_IN");
|
||||
@@ -831,7 +853,7 @@ function PtzControlPanel({
|
||||
<MdZoomIn />
|
||||
</TooltipButton>
|
||||
<TooltipButton
|
||||
label="Zoom out"
|
||||
label={t("ptz.zoom.out.label")}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_OUT");
|
||||
@@ -854,14 +876,19 @@ function PtzControlPanel({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
||||
aria-label="Click in the frame to center the camera"
|
||||
aria-label={t("ptz.move.clickMove.label")}
|
||||
onClick={() => setClickOverlay(!clickOverlay)}
|
||||
>
|
||||
<TbViewfinder />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{clickOverlay ? "Disable" : "Enable"} click to move</p>
|
||||
<p>
|
||||
{clickOverlay
|
||||
? t("ptz.move.clickMove.disable")
|
||||
: t("ptz.move.clickMove.enable")}{" "}
|
||||
click to move
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -872,7 +899,7 @@ function PtzControlPanel({
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenu modal={!isDesktop}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="PTZ camera presets">
|
||||
<Button aria-label={t("ptz.presets")}>
|
||||
<BsThreeDotsVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -894,7 +921,7 @@ function PtzControlPanel({
|
||||
</DropdownMenu>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>PTZ camera presets</p>
|
||||
<p>{t("ptz.presets")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -904,6 +931,7 @@ function PtzControlPanel({
|
||||
}
|
||||
|
||||
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const rankMap = { all: 0, motion: 1, active_objects: 2 };
|
||||
const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
|
||||
const mode = retain?.mode;
|
||||
@@ -918,13 +946,25 @@ function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
||||
? recordRetainMode
|
||||
: alertsRetainMode;
|
||||
|
||||
const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
|
||||
const source =
|
||||
effectiveRetainMode === recordRetainMode
|
||||
? t("camera", { ns: "views/events" })
|
||||
: t("alerts", { ns: "views/events" });
|
||||
|
||||
return effectiveRetainMode !== "all" ? (
|
||||
<div>
|
||||
Your {source} recording retention configuration is set to{" "}
|
||||
<code>mode: {effectiveRetainMode}</code>, so this on-demand recording will
|
||||
only keep segments with {effectiveRetainMode.replaceAll("_", " ")}.
|
||||
<Trans
|
||||
ns="views/live"
|
||||
values={{
|
||||
source,
|
||||
effectiveRetainMode,
|
||||
effectiveRetainModeName: t(
|
||||
"effectiveRetainMode.modes." + effectiveRetainMode,
|
||||
),
|
||||
}}
|
||||
>
|
||||
effectiveRetainMode.notAllTips
|
||||
</Trans>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
@@ -967,6 +1007,8 @@ function FrigateCameraFeatures({
|
||||
supports2WayTalk,
|
||||
cameraEnabled,
|
||||
}: FrigateCameraFeaturesProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
|
||||
const { payload: detectState, send: sendDetect } = useDetectState(
|
||||
camera.name,
|
||||
);
|
||||
@@ -1010,14 +1052,9 @@ function FrigateCameraFeatures({
|
||||
setIsRecording(true);
|
||||
const toastId = toast.success(
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="font-semibold">
|
||||
Started manual on-demand recording.
|
||||
</div>
|
||||
<div className="font-semibold">{t("manualRecording.started")}</div>
|
||||
{!camera.record.enabled || camera.record.alerts.retain.days == 0 ? (
|
||||
<div>
|
||||
Since recording is disabled or restricted in the config for this
|
||||
camera, only a snapshot will be saved.
|
||||
</div>
|
||||
<div>{t("manualRecording.recordDisabledTips")}</div>
|
||||
) : (
|
||||
<OnDemandRetentionMessage camera={camera} />
|
||||
)}
|
||||
@@ -1030,11 +1067,11 @@ function FrigateCameraFeatures({
|
||||
setActiveToastId(toastId);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to start manual on-demand recording.", {
|
||||
toast.error(t("manualRecording.failedToStart"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [camera]);
|
||||
}, [camera, t]);
|
||||
|
||||
const endEvent = useCallback(() => {
|
||||
if (activeToastId) {
|
||||
@@ -1047,16 +1084,16 @@ function FrigateCameraFeatures({
|
||||
});
|
||||
recordingEventIdRef.current = null;
|
||||
setIsRecording(false);
|
||||
toast.success("Ended manual on-demand recording.", {
|
||||
toast.success(t("manualRecording.ended"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to end manual on-demand recording.", {
|
||||
toast.error(t("manualRecording.failedToEnd"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [activeToastId]);
|
||||
}, [activeToastId, t]);
|
||||
|
||||
const handleEventButtonClick = useCallback(() => {
|
||||
if (isRecording) {
|
||||
@@ -1092,7 +1129,9 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={enabledState == "ON" ? LuPower : LuPowerOff}
|
||||
isActive={enabledState == "ON"}
|
||||
title={`${enabledState == "ON" ? "Disable" : "Enable"} Camera`}
|
||||
title={
|
||||
enabledState == "ON" ? t("camera.disable") : t("camera.enable")
|
||||
}
|
||||
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
|
||||
disabled={false}
|
||||
/>
|
||||
@@ -1101,7 +1140,9 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={detectState == "ON" ? MdPersonSearch : MdPersonOff}
|
||||
isActive={detectState == "ON"}
|
||||
title={`${detectState == "ON" ? "Disable" : "Enable"} Detect`}
|
||||
title={
|
||||
detectState == "ON" ? t("detect.disable") : t("detect.enable")
|
||||
}
|
||||
onClick={() => sendDetect(detectState == "ON" ? "OFF" : "ON")}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@@ -1110,7 +1151,11 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={recordState == "ON" ? LuVideo : LuVideoOff}
|
||||
isActive={recordState == "ON"}
|
||||
title={`${recordState == "ON" ? "Disable" : "Enable"} Recording`}
|
||||
title={
|
||||
recordState == "ON"
|
||||
? t("recording.disable")
|
||||
: t("recording.enable")
|
||||
}
|
||||
onClick={() => sendRecord(recordState == "ON" ? "OFF" : "ON")}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@@ -1119,7 +1164,11 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={snapshotState == "ON" ? MdPhotoCamera : MdNoPhotography}
|
||||
isActive={snapshotState == "ON"}
|
||||
title={`${snapshotState == "ON" ? "Disable" : "Enable"} Snapshots`}
|
||||
title={
|
||||
snapshotState == "ON"
|
||||
? t("snapshots.disable")
|
||||
: t("snapshots.enable")
|
||||
}
|
||||
onClick={() => sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@@ -1129,7 +1178,11 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={audioState == "ON" ? LuEar : LuEarOff}
|
||||
isActive={audioState == "ON"}
|
||||
title={`${audioState == "ON" ? "Disable" : "Enable"} Audio Detect`}
|
||||
title={
|
||||
audioState == "ON"
|
||||
? t("audioDetect.disable")
|
||||
: t("audioDetect.enable")
|
||||
}
|
||||
onClick={() => sendAudio(audioState == "ON" ? "OFF" : "ON")}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@@ -1142,7 +1195,11 @@ function FrigateCameraFeatures({
|
||||
autotrackingState == "ON" ? TbViewfinder : TbViewfinderOff
|
||||
}
|
||||
isActive={autotrackingState == "ON"}
|
||||
title={`${autotrackingState == "ON" ? "Disable" : "Enable"} Autotracking`}
|
||||
title={
|
||||
autotrackingState == "ON"
|
||||
? t("autotracking.disable")
|
||||
: t("autotracking.enable")
|
||||
}
|
||||
onClick={() =>
|
||||
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
|
||||
}
|
||||
@@ -1159,7 +1216,7 @@ function FrigateCameraFeatures({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
|
||||
isActive={isRecording}
|
||||
title={`${isRecording ? "Stop" : "Start"} on-demand recording`}
|
||||
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
|
||||
onClick={handleEventButtonClick}
|
||||
disabled={!cameraEnabled}
|
||||
/>
|
||||
@@ -1180,20 +1237,29 @@ function FrigateCameraFeatures({
|
||||
<div className="flex flex-col gap-5 p-4">
|
||||
{!isRestreamed && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Stream</Label>
|
||||
<Label>
|
||||
{t("streaming.label", { ns: "components/dialog" })}
|
||||
</Label>
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>Restreaming is not enabled for this camera.</div>
|
||||
<div>
|
||||
{t("streaming.restreaming.NotEnabled", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
Set up go2rtc for additional live view options and audio
|
||||
for this camera.
|
||||
{t("streaming.restreaming.desc", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@@ -1201,7 +1267,9 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t("streaming.restreaming.readTheDocumentation", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1213,7 +1281,9 @@ function FrigateCameraFeatures({
|
||||
{isRestreamed &&
|
||||
Object.values(camera.live.streams).length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="streaming-method">Stream</Label>
|
||||
<Label htmlFor="streaming-method">
|
||||
{t("stream.title")}
|
||||
</Label>
|
||||
<Select
|
||||
value={streamName}
|
||||
onValueChange={(value) => {
|
||||
@@ -1248,22 +1318,23 @@ function FrigateCameraFeatures({
|
||||
{supportsAudioOutput ? (
|
||||
<>
|
||||
<LuCheck className="size-4 text-success" />
|
||||
<div>Audio is available for this stream</div>
|
||||
<div>{t("stream.audio.available")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>Audio is unavailable for this stream</div>
|
||||
<div>{t("stream.audio.unavailable")}</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
Audio must be output from your camera and
|
||||
configured in go2rtc for this stream.
|
||||
{t("stream.audio.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@@ -1271,7 +1342,7 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t("stream.audio.tips.documentation")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1288,26 +1359,23 @@ function FrigateCameraFeatures({
|
||||
{supports2WayTalk ? (
|
||||
<>
|
||||
<LuCheck className="size-4 text-success" />
|
||||
<div>
|
||||
Two-way talk is available for this stream
|
||||
</div>
|
||||
<div>{t("stream.twoWayTalk.available")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>
|
||||
Two-way talk is unavailable for this stream
|
||||
</div>
|
||||
<div>{t("stream.twoWayTalk.available")}</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
Your device must suppport the feature and
|
||||
WebRTC must be configured for two-way talk.
|
||||
{t("stream.twoWayTalk.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live/#webrtc-extra-configuration"
|
||||
@@ -1315,7 +1383,9 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t(
|
||||
"stream.twoWayTalk.tips.documentation",
|
||||
)}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1332,20 +1402,19 @@ function FrigateCameraFeatures({
|
||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||
|
||||
<p className="text-sm">
|
||||
Live view is in low-bandwidth mode due to buffering
|
||||
or stream errors.
|
||||
{t("stream.lowBandwidth.tips")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
aria-label="Reset the stream"
|
||||
aria-label={t("stream.lowBandwidth.resetStream")}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLowBandwidth(false)}
|
||||
>
|
||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
||||
<div className="text-primary-variant">
|
||||
Reset stream
|
||||
{t("stream.lowBandwidth.resetStream")}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1359,7 +1428,7 @@ function FrigateCameraFeatures({
|
||||
className="mx-0 cursor-pointer text-primary"
|
||||
htmlFor="backgroundplay"
|
||||
>
|
||||
Play in background
|
||||
{t("stream.playInBackground.label")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@@ -1371,8 +1440,7 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enable this option to continue streaming when the player is
|
||||
hidden.
|
||||
{t("stream.playInBackground.tips")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1382,7 +1450,9 @@ function FrigateCameraFeatures({
|
||||
className="mx-0 cursor-pointer text-primary"
|
||||
htmlFor="showstats"
|
||||
>
|
||||
Show stream stats
|
||||
{t("streaming.showStats.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
@@ -1392,13 +1462,12 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enable this option to show stream statistics as an overlay on
|
||||
the camera feed.
|
||||
{t("streaming.showStats.desc", { ns: "components/dialog" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
||||
Debug View
|
||||
{t("streaming.debugView", { ns: "components/dialog" })}
|
||||
<LuExternalLink
|
||||
onClick={() =>
|
||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
||||
@@ -1427,7 +1496,7 @@ function FrigateCameraFeatures({
|
||||
variant="primary"
|
||||
Icon={FaCog}
|
||||
isActive={false}
|
||||
title={`${camera} Settings`}
|
||||
title={t("cameraSettings.title", { camera })}
|
||||
/>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="rounded-2xl px-2 py-4">
|
||||
@@ -1435,14 +1504,14 @@ function FrigateCameraFeatures({
|
||||
{isAdmin && (
|
||||
<>
|
||||
<FilterSwitch
|
||||
label="Camera Enabled"
|
||||
label={t("cameraSettings.cameraEnabled")}
|
||||
isChecked={enabledState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendEnabled(enabledState == "ON" ? "OFF" : "ON")
|
||||
}
|
||||
/>
|
||||
<FilterSwitch
|
||||
label="Object Detection"
|
||||
label={t("cameraSettings.objectDetection")}
|
||||
isChecked={detectState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendDetect(detectState == "ON" ? "OFF" : "ON")
|
||||
@@ -1450,7 +1519,7 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
{recordingEnabled && (
|
||||
<FilterSwitch
|
||||
label="Recording"
|
||||
label={t("cameraSettings.recording")}
|
||||
isChecked={recordState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendRecord(recordState == "ON" ? "OFF" : "ON")
|
||||
@@ -1458,7 +1527,7 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
)}
|
||||
<FilterSwitch
|
||||
label="Snapshots"
|
||||
label={t("cameraSettings.snapshots")}
|
||||
isChecked={snapshotState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
|
||||
@@ -1466,7 +1535,7 @@ function FrigateCameraFeatures({
|
||||
/>
|
||||
{audioDetectEnabled && (
|
||||
<FilterSwitch
|
||||
label="Audio Detection"
|
||||
label={t("cameraSettings.audioDetection")}
|
||||
isChecked={audioState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendAudio(audioState == "ON" ? "OFF" : "ON")
|
||||
@@ -1475,7 +1544,7 @@ function FrigateCameraFeatures({
|
||||
)}
|
||||
{autotrackingEnabled && (
|
||||
<FilterSwitch
|
||||
label="Autotracking"
|
||||
label={t("cameraSettings.autotracking")}
|
||||
isChecked={autotrackingState == "ON"}
|
||||
onCheckedChange={() =>
|
||||
sendAutotracking(autotrackingState == "ON" ? "OFF" : "ON")
|
||||
@@ -1489,20 +1558,27 @@ function FrigateCameraFeatures({
|
||||
<div className="mt-3 flex flex-col gap-5">
|
||||
{!isRestreamed && (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<Label>Stream</Label>
|
||||
<Label>{t("streaming", { ns: "components/dialog" })}</Label>
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>Restreaming is not enabled for this camera.</div>
|
||||
<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">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-xs">
|
||||
Set up go2rtc for additional live view options and audio for
|
||||
this camera.
|
||||
{t("streaming.restreaming.desc", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@@ -1510,7 +1586,9 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t("streaming.restreaming.readTheDocumentation", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1521,7 +1599,7 @@ function FrigateCameraFeatures({
|
||||
)}
|
||||
{isRestreamed && Object.values(camera.live.streams).length > 0 && (
|
||||
<div className="mt-1 p-2">
|
||||
<div className="mb-1 text-sm">Stream</div>
|
||||
<div className="mb-1 text-sm">{t("stream.title")}</div>
|
||||
<Select
|
||||
value={streamName}
|
||||
onValueChange={(value) => {
|
||||
@@ -1555,22 +1633,23 @@ function FrigateCameraFeatures({
|
||||
{supportsAudioOutput ? (
|
||||
<>
|
||||
<LuCheck className="size-4 text-success" />
|
||||
<div>Audio is available for this stream</div>
|
||||
<div>{t("stream.audio.available")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>Audio is unavailable for this stream</div>
|
||||
<div>{t("stream.audio.unavailable")}</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52 text-xs">
|
||||
Audio must be output from your camera and configured
|
||||
in go2rtc for this stream.
|
||||
{t("stream.audio.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live"
|
||||
@@ -1578,7 +1657,7 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t("stream.audio.tips.documentation")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1595,22 +1674,23 @@ function FrigateCameraFeatures({
|
||||
{supports2WayTalk ? (
|
||||
<>
|
||||
<LuCheck className="size-4 text-success" />
|
||||
<div>Two-way talk is available for this stream</div>
|
||||
<div>{t("stream.twoWayTalk.available")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LuX className="size-4 text-danger" />
|
||||
<div>Two-way talk is unavailable for this stream</div>
|
||||
<div>{t("stream.twoWayTalk.unavailable")}</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="cursor-pointer p-0">
|
||||
<LuInfo className="size-4" />
|
||||
<span className="sr-only">Info</span>
|
||||
<span className="sr-only">
|
||||
{t("button.info", { ns: "common" })}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52 text-xs">
|
||||
Your device must suppport the feature and WebRTC
|
||||
must be configured for two-way talk.
|
||||
{t("stream.twoWayTalk.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/live/#webrtc-extra-configuration"
|
||||
@@ -1618,7 +1698,7 @@ function FrigateCameraFeatures({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t("stream.twoWayTalk.tips.documentation")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1633,10 +1713,7 @@ function FrigateCameraFeatures({
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||
|
||||
<p className="text-sm">
|
||||
Live view is in low-bandwidth mode due to buffering or
|
||||
stream errors.
|
||||
</p>
|
||||
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
|
||||
</div>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
@@ -1646,7 +1723,9 @@ function FrigateCameraFeatures({
|
||||
onClick={() => setLowBandwidth(false)}
|
||||
>
|
||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
||||
<div className="text-primary-variant">Reset stream</div>
|
||||
<div className="text-primary-variant">
|
||||
{t("stream.lowBandwidth.resetStream")}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -1654,7 +1733,7 @@ function FrigateCameraFeatures({
|
||||
)}
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<div className="mb-1 text-sm font-medium leading-none">
|
||||
On-Demand Recording
|
||||
{t("manualRecording.title")}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleEventButtonClick}
|
||||
@@ -1663,46 +1742,43 @@ function FrigateCameraFeatures({
|
||||
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
||||
)}
|
||||
>
|
||||
{isRecording ? "End" : "Start"} on-demand recording
|
||||
{t("manualRecording." + isRecording ? "end" : "start")}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Start a manual event based on this camera's recording retention
|
||||
settings.
|
||||
{t("manualRecording.tips")}
|
||||
</p>
|
||||
</div>
|
||||
{isRestreamed && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FilterSwitch
|
||||
label="Play in Background"
|
||||
label={t("manualRecording.playInBackground.label")}
|
||||
isChecked={playInBackground}
|
||||
onCheckedChange={(checked) => {
|
||||
setPlayInBackground(checked);
|
||||
}}
|
||||
/>
|
||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||
Enable this option to continue streaming when the player is
|
||||
hidden.
|
||||
{t("manualRecording.playInBackground.desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FilterSwitch
|
||||
label="Show Stats"
|
||||
label={t("manualRecording.showStats.label")}
|
||||
isChecked={showStats}
|
||||
onCheckedChange={(checked) => {
|
||||
setShowStats(checked);
|
||||
}}
|
||||
/>
|
||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||
Enable this option to show stream statistics as an overlay on
|
||||
the camera feed.
|
||||
{t("manualRecording.showStats.desc")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mb-3 flex flex-col gap-1 px-2">
|
||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
||||
Debug View
|
||||
{t("manualRecording.debugView")}
|
||||
<LuExternalLink
|
||||
onClick={() =>
|
||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
||||
|
||||
@@ -43,6 +43,7 @@ import useCameraLiveMode from "@/hooks/use-camera-live-mode";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import LiveContextMenu from "@/components/menu/LiveContextMenu";
|
||||
import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type LiveDashboardViewProps = {
|
||||
cameras: CameraConfig[];
|
||||
@@ -60,6 +61,8 @@ export default function LiveDashboardView({
|
||||
fullscreen,
|
||||
toggleFullscreen,
|
||||
}: LiveDashboardViewProps) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// layout
|
||||
@@ -471,7 +474,9 @@ export default function LiveDashboardView({
|
||||
}
|
||||
const streamName =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamName ||
|
||||
Object.values(camera.live.streams)?.[0];
|
||||
camera?.live?.streams
|
||||
? Object?.values(camera?.live?.streams)?.[0]
|
||||
: "";
|
||||
const autoLive =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
||||
"no-streaming";
|
||||
@@ -560,7 +565,9 @@ export default function LiveDashboardView({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{fullscreen ? "Exit Fullscreen" : "Fullscreen"}
|
||||
{fullscreen
|
||||
? t("button.exitFullscreen", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user