diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c782fb32ff..6a0f447f13 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,19 +6,8 @@ "initializeCommand": ".devcontainer/initialize.sh", "postCreateCommand": ".devcontainer/post_create.sh", "overrideCommand": false, - "remoteUser": "vscode", - "features": { - "ghcr.io/devcontainers/features/common-utils:2": {} - // Uncomment the following lines to use ONNX Runtime with CUDA support - // "ghcr.io/devcontainers/features/nvidia-cuda:1": { - // "installCudnn": true, - // "installNvtx": true, - // "installToolkit": true, - // "cudaVersion": "12.5", - // "cudnnVersion": "9.4.0.58" - // }, - // "./features/onnxruntime-gpu": {} - }, + "remoteUser": "root", + "features": {}, "forwardPorts": [ 8971, 5000, diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index fcf7ca693f..9bad7f208c 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -13,8 +13,12 @@ fi # Frigate normal container runs as root, so it have permission to create # the folders. But the devcontainer runs as the host user, so we need to # create the folders and give the host user permission to write to them. -sudo mkdir -p /media/frigate -sudo chown -R "$(id -u):$(id -g)" /media/frigate +SUDO="" +if [[ $EUID -ne 0 ]]; then + SUDO="sudo" +fi +$SUDO mkdir -p /media/frigate +$SUDO chown -R "$(id -u):$(id -g)" /media/frigate # When started as a service, LIBAVFORMAT_VERSION_MAJOR is defined in the # s6 service file. For dev, where frigate is started from an interactive diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 272e0f2795..a6e6daa1e1 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 6d52ae154b..b87e408ede 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1187,8 +1187,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 337f37f23a..7a560af9f2 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -23,7 +23,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"; @@ -35,12 +35,12 @@ 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 { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { use24HourTime } from "@/hooks/use-date-utils"; import FilterSwitch from "@/components/filter/FilterSwitch"; @@ -51,6 +51,7 @@ import { useDocDomain } from "@/hooks/use-doc-domain"; import { isPWA } from "@/utils/isPWA"; import { isIOS } from "react-device-detect"; 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"; @@ -756,6 +757,8 @@ export function CameraNotificationSwitch({ } }, [notificationSuspendUntil, notificationState]); + const [customDialogOpen, setCustomDialogOpen] = useState(false); + const handleSuspend = (duration: string) => { setIsSuspended(true); if (duration == "off") { @@ -765,6 +768,11 @@ export function CameraNotificationSwitch({ } }; + const handleCustomSuspend = (totalMinutes: number) => { + setIsSuspended(true); + sendNotificationSuspend(totalMinutes); + }; + const handleCancelSuspension = () => { sendNotification("ON"); sendNotificationSuspend(0); @@ -824,34 +832,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")} +

+ )} + + + + + + + + + ); +}