mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-03 22:04:53 +03:00
Switch to searchEffect implementation
This commit is contained in:
parent
878e81e05b
commit
1c987581ff
@ -23,24 +23,19 @@ import {
|
|||||||
} from "@/utils/dateUtil";
|
} from "@/utils/dateUtil";
|
||||||
import {
|
import {
|
||||||
parseRecordingReviewLink,
|
parseRecordingReviewLink,
|
||||||
RECORDING_REVIEW_START_PARAM,
|
RECORDING_REVIEW_LINK_PARAM,
|
||||||
RECORDING_REVIEW_TIMEZONE_PARAM,
|
|
||||||
} from "@/utils/recordingReviewUrl";
|
} from "@/utils/recordingReviewUrl";
|
||||||
import EventView from "@/views/events/EventView";
|
import EventView from "@/views/events/EventView";
|
||||||
import MotionSearchView from "@/views/motion-search/MotionSearchView";
|
import MotionSearchView from "@/views/motion-search/MotionSearchView";
|
||||||
import { RecordingView } from "@/views/recording/RecordingView";
|
import { RecordingView } from "@/views/recording/RecordingView";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
const { t } = useTranslation(["views/events"]);
|
const { t } = useTranslation(["views/events"]);
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@ -75,7 +70,6 @@ export default function Events() {
|
|||||||
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
|
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const handledReviewLinkRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const motionSearchCameras = useMemo(() => {
|
const motionSearchCameras = useMemo(() => {
|
||||||
if (!config?.cameras) {
|
if (!config?.cameras) {
|
||||||
@ -249,50 +243,20 @@ export default function Events() {
|
|||||||
[recording, setRecording, setReviewFilter],
|
[recording, setRecording, setReviewFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
// shared recording links enter /review through query params, but the
|
useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => {
|
||||||
// existing recording view is opened via router state (`recording`)
|
|
||||||
|
|
||||||
// this effect translates the URL entry point into the state shape the
|
|
||||||
// rest of the page already uses, then cleans the URL back to plain /review
|
|
||||||
useEffect(() => {
|
|
||||||
const timestamp = searchParams.get(RECORDING_REVIEW_START_PARAM);
|
|
||||||
const timezone = searchParams.get(RECORDING_REVIEW_TIMEZONE_PARAM);
|
|
||||||
|
|
||||||
if (!timestamp) {
|
|
||||||
handledReviewLinkRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const camera = location.hash
|
const reviewLink = parseRecordingReviewLink(reviewLinkValue);
|
||||||
? decodeURIComponent(location.hash.substring(1))
|
|
||||||
: null;
|
|
||||||
const reviewLinkKey = `${camera ?? ""}|${timestamp}|${timezone ?? ""}`;
|
|
||||||
|
|
||||||
if (handledReviewLinkRef.current === reviewLinkKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handledReviewLinkRef.current = reviewLinkKey;
|
|
||||||
|
|
||||||
const reviewLink = parseRecordingReviewLink(camera, timestamp, timezone);
|
|
||||||
|
|
||||||
if (!reviewLink) {
|
if (!reviewLink) {
|
||||||
toast.error(t("recordings.invalidSharedLink"), {
|
toast.error(t("recordings.invalidSharedLink"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
navigate(location.pathname, {
|
return true;
|
||||||
state: location.state,
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject unknown or unauthorized cameras before switching into
|
|
||||||
// recording view so bad links cleanly fall back to plain /review
|
|
||||||
const validCamera =
|
const validCamera =
|
||||||
config.cameras[reviewLink.camera] &&
|
config.cameras[reviewLink.camera] &&
|
||||||
allowedCameras.includes(reviewLink.camera);
|
allowedCameras.includes(reviewLink.camera);
|
||||||
@ -301,46 +265,27 @@ export default function Events() {
|
|||||||
toast.error(t("recordings.invalidSharedCamera"), {
|
toast.error(t("recordings.invalidSharedCamera"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
navigate(location.pathname, {
|
return true;
|
||||||
state: location.state,
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextRecording: RecordingStartingPoint = {
|
|
||||||
camera: reviewLink.camera,
|
|
||||||
startTime: reviewLink.timestamp,
|
|
||||||
// severity not actually applicable here, but the type requires it
|
|
||||||
// this pattern is also used LiveCameraView to enter recording view
|
|
||||||
severity: "alert" as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
setReviewFilter({
|
setReviewFilter({
|
||||||
...reviewFilter,
|
...reviewFilter,
|
||||||
...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
|
...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
|
||||||
});
|
});
|
||||||
|
setRecording(
|
||||||
navigate(location.pathname, {
|
{
|
||||||
state: {
|
camera: reviewLink.camera,
|
||||||
...location.state,
|
startTime: reviewLink.timestamp,
|
||||||
recording: nextRecording,
|
// severity not actually applicable here, but the type requires it
|
||||||
|
// this pattern is also used LiveCameraView to enter recording view
|
||||||
|
severity: "alert",
|
||||||
|
timelineType: notificationTab,
|
||||||
},
|
},
|
||||||
replace: true,
|
true,
|
||||||
});
|
);
|
||||||
}, [
|
|
||||||
location.hash,
|
return false;
|
||||||
location.pathname,
|
});
|
||||||
location.state,
|
|
||||||
config,
|
|
||||||
navigate,
|
|
||||||
allowedCameras,
|
|
||||||
getReviewDayBounds,
|
|
||||||
reviewFilter,
|
|
||||||
searchParams,
|
|
||||||
setReviewFilter,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// review paging
|
// review paging
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { formatInTimeZone, fromZonedTime } from "date-fns-tz";
|
import { formatInTimeZone, fromZonedTime } from "date-fns-tz";
|
||||||
|
|
||||||
export const RECORDING_REVIEW_START_PARAM = "start_time";
|
export const RECORDING_REVIEW_LINK_PARAM = "timestamp";
|
||||||
export const RECORDING_REVIEW_TIMEZONE_PARAM = "timezone";
|
|
||||||
|
|
||||||
export type RecordingReviewLinkState = {
|
export type RecordingReviewLinkState = {
|
||||||
camera: string;
|
camera: string;
|
||||||
@ -25,10 +24,14 @@ function formatRecordingReviewTimestamp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseRecordingReviewLink(
|
export function parseRecordingReviewLink(
|
||||||
camera: string | null,
|
value: string | null,
|
||||||
start: string | null,
|
|
||||||
timezone: string | null,
|
|
||||||
): RecordingReviewLinkState | undefined {
|
): RecordingReviewLinkState | undefined {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [camera, start, timezone] = value.split("|");
|
||||||
|
|
||||||
if (!camera || !start) {
|
if (!camera || !start) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -61,9 +64,9 @@ export function createRecordingReviewUrl(
|
|||||||
const normalizedPathname = pathname.startsWith("/")
|
const normalizedPathname = pathname.startsWith("/")
|
||||||
? pathname
|
? pathname
|
||||||
: `/${pathname}`;
|
: `/${pathname}`;
|
||||||
const timezoneParam = timezone
|
const reviewLink = timezone
|
||||||
? `&${RECORDING_REVIEW_TIMEZONE_PARAM}=${encodeURIComponent(timezone)}`
|
? `${state.camera}|${formattedTimestamp}|${timezone}`
|
||||||
: "";
|
: `${state.camera}|${formattedTimestamp}`;
|
||||||
|
|
||||||
return `${url.origin}${normalizedPathname}?${RECORDING_REVIEW_START_PARAM}=${formattedTimestamp}${timezoneParam}#${encodeURIComponent(state.camera)}`;
|
return `${url.origin}${normalizedPathname}?${RECORDING_REVIEW_LINK_PARAM}=${reviewLink}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user