diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 4436808d08..3dce178acc 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -21,6 +21,7 @@ "1hour": "1 hour", "12hours": "12 hours", "24hours": "24 hours", + "custom": "Custom...", "pm": "pm", "am": "am", "yr": "{{time}}yr", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 11fcd92123..2dec5637aa 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1095,8 +1095,15 @@ "1hour": "Suspend for 1 hour", "12hours": "Suspend for 12 hours", "24hours": "Suspend for 24 hours", + "custom": "Suspend until...", "untilRestart": "Suspend until restart" }, + "customSuspension": { + "title": "Custom suspension time", + "description": "Suspend notifications for this camera until the selected time.", + "untilLabel": "Suspend until", + "invalidTime": "Pick a time in the future." + }, "cancelSuspension": "Cancel Suspension", "toast": { "success": { diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index fb53055bcc..12dd3b3dfc 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -24,7 +24,7 @@ import { useState, } from "react"; import { useForm } from "react-hook-form"; -import { LuCheck, LuExternalLink, LuX } from "react-icons/lu"; +import { LuCheck, LuChevronDown, LuExternalLink, LuX } from "react-icons/lu"; import { CiCircleAlert } from "react-icons/ci"; import { Link } from "react-router-dom"; import { toast } from "sonner"; @@ -36,12 +36,13 @@ import { useNotificationTest, } from "@/api/ws"; import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "@/components/ui/select"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { selectTriggerClassName } from "@/components/ui/select"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { use24HourTime } from "@/hooks/use-date-utils"; import FilterSwitch from "@/components/filter/FilterSwitch"; @@ -50,6 +51,7 @@ import { Trans, useTranslation } from "react-i18next"; import { useDateLocale } from "@/hooks/use-date-locale"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import CustomSuspensionDialog from "@/components/overlay/dialog/CustomSuspensionDialog"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { cn } from "@/lib/utils"; import cloneDeep from "lodash/cloneDeep"; @@ -741,6 +743,8 @@ export function CameraNotificationSwitch({ } }, [notificationSuspendUntil, notificationState]); + const [customDialogOpen, setCustomDialogOpen] = useState(false); + const handleSuspend = (duration: string) => { setIsSuspended(true); if (duration == "off") { @@ -750,6 +754,11 @@ export function CameraNotificationSwitch({ } }; + const handleCustomSuspend = (totalMinutes: number) => { + setIsSuspended(true); + sendNotificationSuspend(totalMinutes); + }; + const handleCancelSuspension = () => { sendNotification("ON"); sendNotificationSuspend(0); @@ -809,34 +818,41 @@ export function CameraNotificationSwitch({ {!isSuspended ? ( - + + + setCustomDialogOpen(true)}> + {t("notification.suspendTime.custom")} + + + ) : ( + + + { + if (!day) return; + const next = new Date(day); + const carry = isValidDate(until) ? until : new Date(); + next.setHours( + carry.getHours(), + carry.getMinutes(), + carry.getSeconds(), + 0, + ); + setUntil(next); + setCalendarOpen(false); + }} + /> + + + { + const [h, m] = e.target.value.split(":"); + const hh = Number.parseInt(h ?? "", 10); + const mm = Number.parseInt(m ?? "", 10); + if (Number.isNaN(hh) || Number.isNaN(mm)) return; + const base = isValidDate(until) ? until : new Date(); + const next = new Date(base); + next.setHours(hh, mm, 0, 0); + setUntil(next); + }} + /> + + {!isFuture && ( +

+ {t("notification.customSuspension.invalidTime")} +

+ )} + + + + + + + + + ); +} diff --git a/web/src/components/ui/select.tsx b/web/src/components/ui/select.tsx index 8eabc46518..b6959d38b6 100644 --- a/web/src/components/ui/select.tsx +++ b/web/src/components/ui/select.tsx @@ -10,16 +10,16 @@ const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; +export const selectTriggerClassName = + "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-background_alt [&>span]:line-clamp-1"; + const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( span]:line-clamp-1", - className, - )} + className={cn(selectTriggerClassName, className)} {...props} > {children}