From a347cb5a42bf0c8568476d3ebc3edf25ea1f3eed Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 4 Aug 2024 08:05:26 -0500 Subject: [PATCH 01/16] Fix large tablet recording view layout (#12753) --- web/src/views/events/RecordingView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index ce416b873..c1b30b98e 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -507,7 +507,7 @@ export function RecordingView({ "pt-2 portrait:w-full", mainCameraAspect == "wide" ? "aspect-wide landscape:w-full" - : "aspect-video landscape:h-[94%]", + : "aspect-video landscape:h-[94%] landscape:xl:h-[65%]", ), )} style={{ From 4a867ddd56d9dffc12a83fcba487f9bbeaecfa05 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 4 Aug 2024 08:06:11 -0500 Subject: [PATCH 02/16] Use radix css var to limit desktop menu height (#12743) --- web/src/components/menu/GeneralSettings.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index baab171cc..34637d57e 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -139,8 +139,18 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
From 93b81756c6f26fc5b4916cafbefe7f117e7a066b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 5 Aug 2024 08:01:42 -0500 Subject: [PATCH 03/16] Only use dense property on phones for motion review timeline (#12768) --- web/src/views/events/EventView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index a6c5e6bc8..8b4569be0 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -1057,7 +1057,7 @@ function MotionReview({ setScrubbing(scrubbing); }} - dense={isMobile} + dense={isMobileOnly} /> ) : ( From 5069072a8420ab9a0d872eeb3e7499eb5456e2e0 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 5 Aug 2024 07:20:21 -0600 Subject: [PATCH 04/16] Fix iOS export buttons (#12755) * Fix iOS export buttons * Use layering instead of z index --- web/src/components/card/ExportCard.tsx | 41 +++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index a2c92b5fb..d39cbbeda 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -124,13 +124,27 @@ export default function ExportCard({ onMouseLeave={isDesktop ? () => setHovered(false) : undefined} onClick={isDesktop ? undefined : () => setHovered(!hovered)} > - {hovered && ( + {exportedRecording.in_progress ? ( + + ) : ( <> -
+ {exportedRecording.thumb_path.length > 0 ? ( + setLoading(false)} + /> + ) : ( +
+ )} + + )} + {hovered && ( +
+
{!exportedRecording.in_progress && ( @@ -167,7 +181,7 @@ export default function ExportCard({ {!exportedRecording.in_progress && ( )} - - )} - {exportedRecording.in_progress ? ( - - ) : ( - <> - {exportedRecording.thumb_path.length > 0 ? ( - setLoading(false)} - /> - ) : ( -
- )} - +
)} {loading && ( )} -
+
{exportedRecording.name.replaceAll("_", " ")}
From f8f7b74792cc96b344a5abb53dd61b6936f3928f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 6 Aug 2024 06:40:20 -0600 Subject: [PATCH 05/16] Update version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0cfd9b282..1c1d19a0c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ default_target: local COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) -VERSION = 0.14.0 +VERSION = 0.14.1 IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) CURRENT_UID := $(shell id -u) From 43d2986208982d2219409742986d27fc4687d479 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 6 Aug 2024 09:08:14 -0600 Subject: [PATCH 06/16] Handle case where sub label was null (#12785) --- web/src/components/player/LivePlayer.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 88730b8fb..67057a278 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -266,10 +266,7 @@ export default function LivePlayer({ ), ]), ] - .filter( - (label) => - label !== undefined && !label.includes("-verified"), - ) + .filter((label) => label?.includes("-verified") == false) .map((label) => capitalizeFirstLetter(label)) .sort() .join(", ") From 8212b66ee0f19e1e393b784884abdff6d644b889 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 6 Aug 2024 09:08:43 -0600 Subject: [PATCH 07/16] Use camera status to get state of camera config (#12787) * Use camera status to get state of camera config * Fix spelling --- frigate/comms/dispatcher.py | 15 ++++++++++++- web/src/api/ws.tsx | 44 ++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index db6c44c11..26922f284 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -129,7 +129,20 @@ class Dispatcher: elif topic == UPDATE_CAMERA_ACTIVITY: self.camera_activity = payload elif topic == "onConnect": - self.publish("camera_activity", json.dumps(self.camera_activity)) + camera_status = self.camera_activity.copy() + + for camera in camera_status.keys(): + camera_status[camera]["config"] = { + "detect": self.config.cameras[camera].detect.enabled, + "snapshots": self.config.cameras[camera].snapshots.enabled, + "record": self.config.cameras[camera].record.enabled, + "audio": self.config.cameras[camera].audio.enabled, + "autotracking": self.config.cameras[ + camera + ].onvif.autotracking.enabled, + } + + self.publish("camera_activity", json.dumps(camera_status)) else: self.publish(topic, payload, retain=False) diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index afcbaa0c0..94f381ada 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -1,7 +1,6 @@ import { baseUrl } from "./baseUrl"; import { useCallback, useEffect, useState } from "react"; import useWebSocket, { ReadyState } from "react-use-websocket"; -import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateCameraState, FrigateEvent, @@ -9,7 +8,6 @@ import { ToggleableSetting, } from "@/types/ws"; import { FrigateStats } from "@/types/stats"; -import useSWR from "swr"; import { createContainer } from "react-tracked"; import useDeepMemo from "@/hooks/use-deep-memo"; @@ -26,40 +24,50 @@ type WsState = { type useValueReturn = [WsState, (update: Update) => void]; function useValue(): useValueReturn { - // basic config - const { data: config } = useSWR("config", { - revalidateOnFocus: false, - }); const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; // main state + + const [hasCameraState, setHasCameraState] = useState(false); const [wsState, setWsState] = useState({}); useEffect(() => { - if (!config) { + if (hasCameraState) { + return; + } + + const activityValue: string = wsState["camera_activity"] as string; + + if (!activityValue) { + return; + } + + const cameraActivity: { [key: string]: object } = JSON.parse(activityValue); + + if (!cameraActivity) { return; } const cameraStates: WsState = {}; - Object.keys(config.cameras).forEach((camera) => { - const { name, record, detect, snapshots, audio, onvif } = - config.cameras[camera]; - cameraStates[`${name}/recordings/state`] = record.enabled ? "ON" : "OFF"; - cameraStates[`${name}/detect/state`] = detect.enabled ? "ON" : "OFF"; - cameraStates[`${name}/snapshots/state`] = snapshots.enabled - ? "ON" - : "OFF"; - cameraStates[`${name}/audio/state`] = audio.enabled ? "ON" : "OFF"; - cameraStates[`${name}/ptz_autotracker/state`] = onvif.autotracking.enabled + Object.entries(cameraActivity).forEach(([name, state]) => { + const { record, detect, snapshots, audio, autotracking } = + // @ts-expect-error we know this is correct + state["config"]; + cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF"; + cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF"; + cameraStates[`${name}/snapshots/state`] = snapshots ? "ON" : "OFF"; + cameraStates[`${name}/audio/state`] = audio ? "ON" : "OFF"; + cameraStates[`${name}/ptz_autotracker/state`] = autotracking ? "ON" : "OFF"; }); setWsState({ ...wsState, ...cameraStates }); + setHasCameraState(true); // we only want this to run initially when the config is loaded // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]); + }, [wsState]); // ws handler const { sendJsonMessage, readyState } = useWebSocket(wsUrl, { From 54e1bd9eeb1dd5419c4de30bb7972cdc8aafe5bf Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:41:11 -0500 Subject: [PATCH 08/16] Ensure review cameras are sorted by config ui order if specified (#12789) --- web/src/components/filter/ReviewFilterGroup.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 5ee0ac9bb..d01ea384f 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -136,7 +136,11 @@ export default function ReviewFilterGroup({ const filterValues = useMemo( () => ({ - cameras: Object.keys(config?.cameras || {}), + cameras: Object.keys(config?.cameras ?? {}).sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), }), From 9c2974438d6c78d4ab6b2bb0988225bf4da6e1bd Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 6 Aug 2024 14:15:00 -0600 Subject: [PATCH 09/16] Handle case where user stops scrubbing but remains hovering (#12794) * Handle case where user stops scrubbing but remains hovering * Add type --- .../player/PreviewThumbnailPlayer.tsx | 148 +++++++++++------- web/src/types/timeline.ts | 2 + 2 files changed, 94 insertions(+), 56 deletions(-) diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 20eefe361..d2192e85d 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -21,7 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import useContextMenu from "@/hooks/use-contextmenu"; import ActivityIndicator from "../indicators/activity-indicator"; -import { TimeRange } from "@/types/timeline"; +import { TimelineScrubMode, TimeRange } from "@/types/timeline"; import { NoThumbSlider } from "../ui/slider"; import { PREVIEW_FPS, PREVIEW_PADDING } from "@/types/preview"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; @@ -414,7 +414,7 @@ export function VideoPreview({ if (isSafari || (isFirefox && isMobile)) { playerRef.current.pause(); - setManualPlayback(true); + setPlaybackMode("compat"); } else { playerRef.current.currentTime = playerStartTime; playerRef.current.playbackRate = PREVIEW_FPS; @@ -453,9 +453,9 @@ export function VideoPreview({ setReviewed(); if (loop && playerRef.current) { - if (manualPlayback) { - setManualPlayback(false); - setTimeout(() => setManualPlayback(true), 100); + if (playbackMode != "auto") { + setPlaybackMode("auto"); + setTimeout(() => setPlaybackMode("compat"), 100); } playerRef.current.currentTime = playerStartTime; @@ -472,7 +472,7 @@ export function VideoPreview({ playerRef.current?.pause(); } - setManualPlayback(false); + setPlaybackMode("auto"); setProgress(100.0); } else { setProgress(playerPercent); @@ -486,9 +486,10 @@ export function VideoPreview({ // safari is incapable of playing at a speed > 2x // so manual seeking is required on iOS - const [manualPlayback, setManualPlayback] = useState(false); + const [playbackMode, setPlaybackMode] = useState("auto"); + useEffect(() => { - if (!manualPlayback || !playerRef.current) { + if (playbackMode != "compat" || !playerRef.current) { return; } @@ -503,10 +504,14 @@ export function VideoPreview({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [manualPlayback, playerRef]); + }, [playbackMode, playerRef]); // user interaction + useEffect(() => { + setIgnoreClick(playbackMode != "auto" && playbackMode != "compat"); + }, [playbackMode, setIgnoreClick]); + const onManualSeek = useCallback( (values: number[]) => { const value = values[0]; @@ -515,14 +520,8 @@ export function VideoPreview({ return; } - if (manualPlayback) { - setManualPlayback(false); - setIgnoreClick(true); - } - if (playerRef.current.paused == false) { playerRef.current.pause(); - setIgnoreClick(true); } if (setReviewed) { @@ -536,27 +535,21 @@ export function VideoPreview({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - [ - manualPlayback, - playerDuration, - playerRef, - playerStartTime, - setIgnoreClick, - ], + [playerDuration, playerRef, playerStartTime, setIgnoreClick], ); const onStopManualSeek = useCallback(() => { setTimeout(() => { - setIgnoreClick(false); setHoverTimeout(undefined); if (isSafari || (isFirefox && isMobile)) { - setManualPlayback(true); + setPlaybackMode("compat"); } else { + setPlaybackMode("auto"); playerRef.current?.play(); } }, 500); - }, [playerRef, setIgnoreClick]); + }, [playerRef]); const onProgressHover = useCallback( (event: React.MouseEvent) => { @@ -572,10 +565,8 @@ export function VideoPreview({ if (hoverTimeout) { clearTimeout(hoverTimeout); } - - setHoverTimeout(setTimeout(() => onStopManualSeek(), 500)); }, - [sliderRef, hoverTimeout, onManualSeek, onStopManualSeek, setHoverTimeout], + [sliderRef, hoverTimeout, onManualSeek], ); return ( @@ -597,14 +588,37 @@ export function VideoPreview({ {showProgress && ( { + setPlaybackMode("drag"); + onManualSeek(event); + }} onValueCommit={onStopManualSeek} min={0} step={1} max={100} - onMouseMove={isMobile ? undefined : onProgressHover} + onMouseMove={ + isMobile + ? undefined + : (event) => { + if (playbackMode != "drag") { + setPlaybackMode("hover"); + onProgressHover(event); + } + } + } + onMouseLeave={ + isMobile + ? undefined + : () => { + if (!sliderRef.current) { + return; + } + + setHoverTimeout(setTimeout(() => onStopManualSeek(), 500)); + } + } /> )}
@@ -642,7 +656,8 @@ export function InProgressPreview({ }/frames`, { revalidateOnFocus: false }, ); - const [manualFrame, setManualFrame] = useState(false); + + const [playbackMode, setPlaybackMode] = useState("auto"); const [hoverTimeout, setHoverTimeout] = useState(); const [key, setKey] = useState(0); @@ -655,7 +670,7 @@ export function InProgressPreview({ onTimeUpdate(review.start_time - PREVIEW_PADDING + key); } - if (manualFrame) { + if (playbackMode != "auto") { return; } @@ -692,19 +707,18 @@ export function InProgressPreview({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [key, manualFrame, previewFrames]); + }, [key, playbackMode, previewFrames]); // user interaction + useEffect(() => { + setIgnoreClick(playbackMode != "auto"); + }, [playbackMode, setIgnoreClick]); + const onManualSeek = useCallback( (values: number[]) => { const value = values[0]; - if (!manualFrame) { - setManualFrame(true); - setIgnoreClick(true); - } - if (!review.has_been_reviewed) { setReviewed(review.id); } @@ -714,19 +728,18 @@ export function InProgressPreview({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - [manualFrame, setIgnoreClick, setManualFrame, setKey], + [setIgnoreClick, setKey], ); const onStopManualSeek = useCallback( (values: number[]) => { const value = values[0]; setTimeout(() => { - setIgnoreClick(false); - setManualFrame(false); + setPlaybackMode("auto"); setKey(value - 1); }, 500); }, - [setManualFrame, setIgnoreClick], + [setPlaybackMode], ); const onProgressHover = useCallback( @@ -744,17 +757,8 @@ export function InProgressPreview({ if (hoverTimeout) { clearTimeout(hoverTimeout); } - - setHoverTimeout(setTimeout(() => onStopManualSeek(progress), 500)); }, - [ - sliderRef, - hoverTimeout, - previewFrames, - onManualSeek, - onStopManualSeek, - setHoverTimeout, - ], + [sliderRef, hoverTimeout, previewFrames, onManualSeek], ); if (!previewFrames || previewFrames.length == 0) { @@ -776,14 +780,46 @@ export function InProgressPreview({ {showProgress && ( { + setPlaybackMode("drag"); + onManualSeek(event); + }} onValueCommit={onStopManualSeek} min={0} step={1} max={previewFrames.length - 1} - onMouseMove={isMobile ? undefined : onProgressHover} + onMouseMove={ + isMobile + ? undefined + : (event) => { + if (playbackMode != "drag") { + setPlaybackMode("hover"); + onProgressHover(event); + } + } + } + onMouseLeave={ + isMobile + ? undefined + : (event) => { + if (!sliderRef.current || !previewFrames) { + return; + } + + const rect = sliderRef.current.getBoundingClientRect(); + const positionX = event.clientX - rect.left; + const width = sliderRef.current.clientWidth; + const progress = [ + Math.round((positionX / width) * previewFrames.length), + ]; + + setHoverTimeout( + setTimeout(() => onStopManualSeek(progress), 500), + ); + } + } /> )}
diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index b4e02304c..b7945314d 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -26,3 +26,5 @@ export type Timeline = { export type TimeRange = { before: number; after: number }; export type TimelineType = "timeline" | "events"; + +export type TimelineScrubMode = "auto" | "drag" | "hover" | "compat"; From e563692fa2810c6d9ebf11a247296a197eea927a Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 6 Aug 2024 16:11:20 -0600 Subject: [PATCH 10/16] Add camera name to audio debug line (#12799) * Add camera name to audio debug line * Formatting --- frigate/events/audio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index fca16f364..471d403b8 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -209,7 +209,9 @@ class AudioEventMaintainer(threading.Thread): audio_detections = [] for label, score, _ in model_detections: - logger.debug(f"Heard {label} with a score of {score}") + logger.debug( + f"{self.config.name} heard {label} with a score of {score}" + ) if label not in self.config.audio.listen: continue From 57503cc318fddb3165bacb2de1f1dea3fbf87060 Mon Sep 17 00:00:00 2001 From: Marc Altmann <40744649+MarcA711@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:28:12 +0200 Subject: [PATCH 11/16] fix default model for rknn detector (#12807) --- frigate/detectors/plugins/rknn.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py index af22ca358..7606313b5 100644 --- a/frigate/detectors/plugins/rknn.py +++ b/frigate/detectors/plugins/rknn.py @@ -23,7 +23,6 @@ model_chache_dir = "/config/model_cache/rknn_cache/" class RknnDetectorConfig(BaseDetectorConfig): type: Literal[DETECTOR_KEY] num_cores: int = Field(default=0, ge=0, le=3, title="Number of NPU cores to use.") - purge_model_cache: bool = Field(default=True) class Rknn(DetectionApi): @@ -36,7 +35,9 @@ class Rknn(DetectionApi): core_mask = 2**config.num_cores - 1 soc = self.get_soc() - model_props = self.parse_model_input(config.model.path, soc) + model_path = config.model.path or "deci-fp16-yolonas_s" + + model_props = self.parse_model_input(model_path, soc) if model_props["preset"]: config.model.model_type = model_props["model_type"] From 9f43d10ba71084b72c7fd350c1be875d73191613 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:31:39 -0500 Subject: [PATCH 12/16] Ensure review card icon color for event view is visible in light mode (#12812) --- web/src/components/card/ReviewCard.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index fe221e112..64f52bd1b 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -130,10 +130,16 @@ export default function ReviewCard({
{event.data.objects.map((object) => { - return getIconForLabel(object, "size-3 text-white"); + return getIconForLabel( + object, + "size-3 text-primary dark:text-white", + ); })} {event.data.audio.map((audio) => { - return getIconForLabel(audio, "size-3 text-white"); + return getIconForLabel( + audio, + "size-3 text-primary dark:text-white", + ); })}
{formattedDate}
From 33e04fe61fc75472f55563766ee020c7f376de1b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Aug 2024 07:46:18 -0500 Subject: [PATCH 13/16] Add right click to delete points in desktop mask/zone editor (#12744) --- web/src/components/settings/PolygonCanvas.tsx | 26 +++++++++++++++++++ .../settings/PolygonEditControls.tsx | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/web/src/components/settings/PolygonCanvas.tsx b/web/src/components/settings/PolygonCanvas.tsx index 6121293e5..e6851b63c 100644 --- a/web/src/components/settings/PolygonCanvas.tsx +++ b/web/src/components/settings/PolygonCanvas.tsx @@ -114,6 +114,29 @@ export function PolygonCanvas({ const mousePos = stage.getPointerPosition() ?? { x: 0, y: 0 }; const intersection = stage.getIntersection(mousePos); + // right click on desktops to delete a point + if ( + e.evt instanceof MouseEvent && + e.evt.button === 2 && + intersection?.getClassName() == "Circle" + ) { + const pointIndex = parseInt(intersection.name()?.split("-")[1]); + if (!isNaN(pointIndex)) { + const updatedPoints = activePolygon.points.filter( + (_, index) => index !== pointIndex, + ); + updatedPolygons[activePolygonIndex] = { + ...activePolygon, + points: updatedPoints, + pointsOrder: activePolygon.pointsOrder?.filter( + (_, index) => index !== pointIndex, + ), + }; + setPolygons(updatedPolygons); + } + return; + } + if ( activePolygon.points.length >= 3 && intersection?.getClassName() == "Circle" && @@ -236,6 +259,9 @@ export function PolygonCanvas({ onMouseDown={handleMouseDown} onTouchStart={handleMouseDown} onMouseOver={handleStageMouseOver} + onContextMenu={(e) => { + e.evt.preventDefault(); + }} > - Undo + Remove last point From 6d9590b4ec63c6649b05914ac0c08ae0853f2988 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Aug 2024 07:46:39 -0500 Subject: [PATCH 14/16] Persist live view muted/unmuted for session only (#12727) * Persist live view muted/unmuted for session only * consistent naming --- web/src/hooks/use-session-persistence.ts | 39 ++++++++++++++++++++++++ web/src/views/live/LiveCameraView.tsx | 5 +-- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 web/src/hooks/use-session-persistence.ts diff --git a/web/src/hooks/use-session-persistence.ts b/web/src/hooks/use-session-persistence.ts new file mode 100644 index 000000000..3662c799c --- /dev/null +++ b/web/src/hooks/use-session-persistence.ts @@ -0,0 +1,39 @@ +import { useCallback, useState } from "react"; + +type useSessionPersistenceReturn = [ + value: S | undefined, + setValue: (value: S | undefined) => void, +]; + +export function useSessionPersistence( + key: string, + defaultValue: S | undefined = undefined, +): useSessionPersistenceReturn { + const [storedValue, setStoredValue] = useState(() => { + try { + const value = window.sessionStorage.getItem(key); + + if (value) { + return JSON.parse(value); + } else { + window.sessionStorage.setItem(key, JSON.stringify(defaultValue)); + return defaultValue; + } + } catch (err) { + return defaultValue; + } + }); + + const setValue = useCallback( + (newValue: S | undefined) => { + try { + window.sessionStorage.setItem(key, JSON.stringify(newValue)); + // eslint-disable-next-line no-empty + } catch (err) {} + setStoredValue(newValue); + }, + [key], + ); + + return [storedValue, setValue]; +} diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 033b0d71c..cd7190e86 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -78,6 +78,7 @@ import { useNavigate } from "react-router-dom"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; import useSWR from "swr"; import { cn } from "@/lib/utils"; +import { useSessionPersistence } from "@/hooks/use-session-persistence"; type LiveCameraViewProps = { config?: FrigateConfig; @@ -194,7 +195,7 @@ export default function LiveCameraView({ // playback state - const [audio, setAudio] = useState(false); + const [audio, setAudio] = useSessionPersistence("liveAudio", false); const [mic, setMic] = useState(false); const [webRTC, setWebRTC] = useState(false); const [pip, setPip] = useState(false); @@ -404,7 +405,7 @@ export default function LiveCameraView({ className="p-2 md:p-0" variant={fullscreen ? "overlay" : "primary"} Icon={audio ? GiSpeaker : GiSpeakerOff} - isActive={audio} + isActive={audio ?? false} title={`${audio ? "Disable" : "Enable"} Camera Audio`} onClick={() => setAudio(!audio)} /> From c84511de164b4791194f719d964ef51bcd61bc1e Mon Sep 17 00:00:00 2001 From: "Soren L. Hansen" Date: Fri, 9 Aug 2024 06:26:26 -0700 Subject: [PATCH 15/16] Fix auth when serving Frigate at a subpath (#12815) Ensure axios.defaults.baseURL is set when accessing login form. Drop `/api` prefix in login form's `axios.post` call, since `/api` is part of the baseURL. Redirect to subpath on succesful authentication. Prepend subpath to default logout url. Fixes #12814 --- web/src/components/auth/AuthForm.tsx | 5 +++-- web/src/components/menu/AccountSettings.tsx | 3 ++- web/src/login.tsx | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx index 805085774..f3a435828 100644 --- a/web/src/components/auth/AuthForm.tsx +++ b/web/src/components/auth/AuthForm.tsx @@ -2,6 +2,7 @@ import * as React from "react"; +import { baseUrl } from "../../api/baseUrl"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -43,7 +44,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { setIsLoading(true); try { await axios.post( - "/api/login", + "/login", { user: values.user, password: values.password, @@ -54,7 +55,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { }, }, ); - window.location.href = "/"; + window.location.href = baseUrl; } catch (error) { if (axios.isAxiosError(error)) { const err = error as AxiosError; diff --git a/web/src/components/menu/AccountSettings.tsx b/web/src/components/menu/AccountSettings.tsx index 20c852e65..1b7470b9b 100644 --- a/web/src/components/menu/AccountSettings.tsx +++ b/web/src/components/menu/AccountSettings.tsx @@ -3,6 +3,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { baseUrl } from "../../api/baseUrl"; import { cn } from "@/lib/utils"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { isDesktop } from "react-device-detect"; @@ -26,7 +27,7 @@ type AccountSettingsProps = { export default function AccountSettings({ className }: AccountSettingsProps) { const { data: profile } = useSWR("profile"); const { data: config } = useSWR("config"); - const logoutUrl = config?.proxy?.logout_url || "/api/logout"; + const logoutUrl = config?.proxy?.logout_url || `${baseUrl}api/logout`; const Container = isDesktop ? DropdownMenu : Drawer; const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; diff --git a/web/src/login.tsx b/web/src/login.tsx index cea5e3e42..4906b58c6 100644 --- a/web/src/login.tsx +++ b/web/src/login.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import LoginPage from "@/pages/LoginPage.tsx"; +import "@/api"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( From 024e0a298043071310a5d1a1db7fdb9f977a52c4 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:16:27 -0500 Subject: [PATCH 16/16] Camera group icon in reference config --- docs/docs/configuration/reference.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index a90a3241e..2cbbc4903 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -613,8 +613,8 @@ cameras: user: admin # Optional: password for login. password: admin - # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. - # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. + # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. + # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. ignore_time_mismatch: False # Optional: PTZ camera object autotracking. Keeps a moving object in # the center of the frame by automatically moving the PTZ camera. @@ -719,7 +719,7 @@ camera_groups: - side_cam - front_doorbell_cam # Required: icon used for group - icon: car + icon: LuCar # Required: index of this group order: 0 ```