2024-04-19 14:34:07 +03:00
|
|
|
import Heading from "@/components/ui/heading";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
2024-05-07 17:28:10 +03:00
|
|
|
import { useCallback, useEffect } from "react";
|
|
|
|
|
import { Toaster } from "sonner";
|
|
|
|
|
import { toast } from "sonner";
|
2024-05-29 17:01:39 +03:00
|
|
|
import { Separator } from "../../components/ui/separator";
|
|
|
|
|
import { Button } from "../../components/ui/button";
|
2024-05-07 17:28:10 +03:00
|
|
|
import useSWR from "swr";
|
|
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
|
|
|
import { del as delData } from "idb-keyval";
|
2024-05-14 16:38:03 +03:00
|
|
|
import { usePersistence } from "@/hooks/use-persistence";
|
|
|
|
|
import { isSafari } from "react-device-detect";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectGroup,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
2024-05-29 17:01:39 +03:00
|
|
|
} from "../../components/ui/select";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { useTranslation } from "react-i18next";
|
2024-05-14 16:38:03 +03:00
|
|
|
|
|
|
|
|
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
2024-07-17 19:38:12 +03:00
|
|
|
const WEEK_STARTS_ON = ["Sunday", "Monday"];
|
2024-04-19 14:34:07 +03:00
|
|
|
|
2024-10-16 03:25:59 +03:00
|
|
|
export default function UiSettingsView() {
|
2024-05-07 17:28:10 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation("views/settings");
|
2024-05-07 17:28:10 +03:00
|
|
|
const clearStoredLayouts = useCallback(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Object.entries(config.camera_groups).forEach(async (value) => {
|
|
|
|
|
await delData(`${value[0]}-draggable-layout`)
|
|
|
|
|
.then(() => {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.success(
|
|
|
|
|
t("general.toast.success.clearStoredLayout", {
|
|
|
|
|
cameraName: value[0],
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-05-07 17:28:10 +03:00
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2025-03-08 19:01:08 +03:00
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(
|
|
|
|
|
t("general.toast.error.clearStoredLayoutFailed", { errorMessage }),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-05-07 17:28:10 +03:00
|
|
|
});
|
|
|
|
|
});
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [config, t]);
|
2024-05-07 17:28:10 +03:00
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
const clearStreamingSettings = useCallback(async () => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await delData(`streaming-settings`)
|
|
|
|
|
.then(() => {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.success(t("general.toast.success.clearStreamingSettings"), {
|
2025-02-10 19:42:35 +03:00
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2025-03-08 19:01:08 +03:00
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(
|
|
|
|
|
t("general.toast.error.clearStreamingSettingsFailed", {
|
|
|
|
|
errorMessage,
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-02-10 19:42:35 +03:00
|
|
|
});
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [config, t]);
|
2025-02-10 19:42:35 +03:00
|
|
|
|
2024-04-27 20:02:01 +03:00
|
|
|
useEffect(() => {
|
2025-03-16 18:36:20 +03:00
|
|
|
document.title = t("documentTitle.general");
|
|
|
|
|
}, [t]);
|
2024-04-27 20:02:01 +03:00
|
|
|
|
2024-05-29 17:01:39 +03:00
|
|
|
// settings
|
|
|
|
|
|
|
|
|
|
const [autoLive, setAutoLive] = usePersistence("autoLiveView", true);
|
2025-10-29 17:20:11 +03:00
|
|
|
const [cameraNames, setCameraName] = usePersistence(
|
|
|
|
|
"displayCameraNames",
|
|
|
|
|
false,
|
|
|
|
|
);
|
2024-05-14 16:38:03 +03:00
|
|
|
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
2024-07-17 19:38:12 +03:00
|
|
|
const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0);
|
2024-07-25 16:34:39 +03:00
|
|
|
const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true);
|
2025-11-12 02:00:54 +03:00
|
|
|
const [fallbackTimeout, setFallbackTimeout] = usePersistence(
|
|
|
|
|
"liveFallbackTimeout",
|
|
|
|
|
3,
|
|
|
|
|
);
|
2024-05-14 16:38:03 +03:00
|
|
|
|
2024-04-19 14:34:07 +03:00
|
|
|
return (
|
|
|
|
|
<>
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="flex size-full flex-col md:flex-row">
|
2024-05-07 17:28:10 +03:00
|
|
|
<Toaster position="top-center" closeButton={true} />
|
2025-11-24 16:34:56 +03:00
|
|
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
2025-10-08 22:59:21 +03:00
|
|
|
<Heading as="h4" className="mb-2">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.title")}
|
2024-05-07 17:28:10 +03:00
|
|
|
</Heading>
|
|
|
|
|
|
2024-05-29 17:01:39 +03:00
|
|
|
<Separator className="my-2 flex bg-secondary" />
|
|
|
|
|
|
|
|
|
|
<Heading as="h4" className="my-2">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.liveDashboard.title")}
|
2024-05-29 17:01:39 +03:00
|
|
|
</Heading>
|
|
|
|
|
|
|
|
|
|
<div className="mt-2 space-y-6">
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex flex-row items-center justify-start gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="auto-live"
|
|
|
|
|
checked={autoLive}
|
|
|
|
|
onCheckedChange={setAutoLive}
|
|
|
|
|
/>
|
|
|
|
|
<Label className="cursor-pointer" htmlFor="auto-live">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.liveDashboard.automaticLiveView.label")}
|
2024-05-29 17:01:39 +03:00
|
|
|
</Label>
|
|
|
|
|
</div>
|
2025-02-10 19:42:35 +03:00
|
|
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
2025-03-16 18:36:20 +03:00
|
|
|
<p>{t("general.liveDashboard.automaticLiveView.desc")}</p>
|
2024-05-29 17:01:39 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-07-25 16:34:39 +03:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex flex-row items-center justify-start gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="images-only"
|
|
|
|
|
checked={alertVideos}
|
|
|
|
|
onCheckedChange={setAlertVideos}
|
|
|
|
|
/>
|
|
|
|
|
<Label className="cursor-pointer" htmlFor="images-only">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.liveDashboard.playAlertVideos.label")}
|
2024-07-25 16:34:39 +03:00
|
|
|
</Label>
|
|
|
|
|
</div>
|
2025-02-10 19:42:35 +03:00
|
|
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
2025-03-16 18:36:20 +03:00
|
|
|
<p>{t("general.liveDashboard.playAlertVideos.desc")}</p>
|
2024-07-25 16:34:39 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-29 17:20:11 +03:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex flex-row items-center justify-start gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="camera-names"
|
|
|
|
|
checked={cameraNames}
|
|
|
|
|
onCheckedChange={setCameraName}
|
|
|
|
|
/>
|
2025-11-14 18:23:43 +03:00
|
|
|
<Label className="cursor-pointer" htmlFor="camera-names">
|
2025-10-29 17:20:11 +03:00
|
|
|
{t("general.liveDashboard.displayCameraNames.label")}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
|
|
|
|
<p>{t("general.liveDashboard.displayCameraNames.desc")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-12 02:00:54 +03:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex flex-row items-center justify-start gap-2">
|
|
|
|
|
<Label
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
htmlFor="live-fallback-timeout"
|
|
|
|
|
>
|
|
|
|
|
{t("general.liveDashboard.liveFallbackTimeout.label")}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
|
|
|
|
<p>{t("general.liveDashboard.liveFallbackTimeout.desc")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Select
|
|
|
|
|
value={fallbackTimeout?.toString()}
|
|
|
|
|
onValueChange={(value) => setFallbackTimeout(parseInt(value))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-36">
|
|
|
|
|
{t("time.second", {
|
|
|
|
|
ns: "common",
|
|
|
|
|
time: fallbackTimeout,
|
|
|
|
|
count: fallbackTimeout,
|
|
|
|
|
})}
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectGroup>
|
|
|
|
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((timeout) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={timeout}
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
value={timeout.toString()}
|
|
|
|
|
>
|
|
|
|
|
{t("time.second", {
|
|
|
|
|
ns: "common",
|
|
|
|
|
time: timeout,
|
|
|
|
|
count: timeout,
|
|
|
|
|
})}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectGroup>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
2024-05-29 17:01:39 +03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="my-3 flex w-full flex-col space-y-6">
|
2025-02-10 19:42:35 +03:00
|
|
|
<div className="mt-2 space-y-3">
|
2024-05-07 17:28:10 +03:00
|
|
|
<div className="space-y-0.5">
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="text-md">
|
|
|
|
|
{t("general.storedLayouts.title")}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="my-2 text-sm text-muted-foreground">
|
|
|
|
|
<p>{t("general.storedLayouts.desc")}</p>
|
2024-05-07 17:28:10 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-10-23 01:07:42 +03:00
|
|
|
<Button
|
2025-03-16 18:36:20 +03:00
|
|
|
aria-label={t("general.storedLayouts.clearAll")}
|
2024-10-23 01:07:42 +03:00
|
|
|
onClick={clearStoredLayouts}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.storedLayouts.clearAll")}
|
2024-10-23 01:07:42 +03:00
|
|
|
</Button>
|
2024-05-07 17:28:10 +03:00
|
|
|
</div>
|
2024-05-29 17:01:39 +03:00
|
|
|
|
2025-02-10 19:42:35 +03:00
|
|
|
<div className="mt-2 space-y-3">
|
|
|
|
|
<div className="space-y-0.5">
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="text-md">
|
|
|
|
|
{t("general.cameraGroupStreaming.title")}
|
|
|
|
|
</div>
|
2025-02-10 19:42:35 +03:00
|
|
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
2025-03-16 18:36:20 +03:00
|
|
|
<p>{t("general.cameraGroupStreaming.desc")}</p>
|
2025-02-10 19:42:35 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
2025-03-16 18:36:20 +03:00
|
|
|
aria-label={t("general.cameraGroupStreaming.clearAll")}
|
2025-02-10 19:42:35 +03:00
|
|
|
onClick={clearStreamingSettings}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.cameraGroupStreaming.clearAll")}
|
2025-02-10 19:42:35 +03:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-05-14 18:06:44 +03:00
|
|
|
<Separator className="my-2 flex bg-secondary" />
|
2024-05-29 17:01:39 +03:00
|
|
|
|
|
|
|
|
<Heading as="h4" className="my-2">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.recordingsViewer.title")}
|
2024-05-29 17:01:39 +03:00
|
|
|
</Heading>
|
|
|
|
|
|
2024-05-14 16:38:03 +03:00
|
|
|
<div className="mt-2 space-y-6">
|
|
|
|
|
<div className="space-y-0.5">
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="text-md">
|
|
|
|
|
{t("general.recordingsViewer.defaultPlaybackRate.label")}
|
|
|
|
|
</div>
|
2024-05-18 19:36:13 +03:00
|
|
|
<div className="my-2 text-sm text-muted-foreground">
|
2025-03-16 18:36:20 +03:00
|
|
|
<p>
|
|
|
|
|
{t("general.recordingsViewer.defaultPlaybackRate.desc")}
|
|
|
|
|
</p>
|
2024-05-14 16:38:03 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Select
|
|
|
|
|
value={playbackRate?.toString()}
|
|
|
|
|
onValueChange={(value) => setPlaybackRate(parseFloat(value))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-20">
|
|
|
|
|
{`${playbackRate}x`}
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectGroup>
|
|
|
|
|
{PLAYBACK_RATE_DEFAULT.map((rate) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={rate}
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
value={rate.toString()}
|
|
|
|
|
>
|
|
|
|
|
{rate}x
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectGroup>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2024-05-18 19:36:13 +03:00
|
|
|
<Separator className="my-2 flex bg-secondary" />
|
2024-07-17 19:38:12 +03:00
|
|
|
|
|
|
|
|
<Heading as="h4" className="my-2">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.calendar.title")}
|
2024-07-17 19:38:12 +03:00
|
|
|
</Heading>
|
|
|
|
|
|
|
|
|
|
<div className="mt-2 space-y-6">
|
|
|
|
|
<div className="space-y-0.5">
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="text-md">
|
|
|
|
|
{t("general.calendar.firstWeekday.label")}
|
|
|
|
|
</div>
|
2024-07-17 19:38:12 +03:00
|
|
|
<div className="my-2 text-sm text-muted-foreground">
|
2025-03-16 18:36:20 +03:00
|
|
|
<p>{t("general.calendar.firstWeekday.desc")}</p>
|
2024-07-17 19:38:12 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Select
|
|
|
|
|
value={weekStartsOn?.toString()}
|
|
|
|
|
onValueChange={(value) => setWeekStartsOn(parseInt(value))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-32">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t(
|
|
|
|
|
"general.calendar.firstWeekday." +
|
|
|
|
|
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
|
|
|
|
|
)}
|
2024-07-17 19:38:12 +03:00
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectGroup>
|
|
|
|
|
{WEEK_STARTS_ON.map((day, index) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={index}
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
value={index.toString()}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("general.calendar.firstWeekday." + day.toLowerCase())}
|
2024-07-17 19:38:12 +03:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectGroup>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<Separator className="my-2 flex bg-secondary" />
|
2024-05-07 17:28:10 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-04-19 14:34:07 +03:00
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|