mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-10 09:07:37 +03:00
Store Review filters in URL params
This commit is contained in:
parent
5fdb56a106
commit
72e7f7c834
@ -1,6 +1,6 @@
|
|||||||
import { FilterType } from "@/types/filter";
|
import { FilterType } from "@/types/filter";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
function getStringifiedArgs(filter: FilterType) {
|
function getStringifiedArgs(filter: FilterType) {
|
||||||
const search: { [key: string]: string } = {};
|
const search: { [key: string]: string } = {};
|
||||||
@ -49,12 +49,49 @@ export default function useApiFilter<
|
|||||||
|
|
||||||
export function useApiFilterArgs<F extends FilterType>(
|
export function useApiFilterArgs<F extends FilterType>(
|
||||||
arrayKeys: string[],
|
arrayKeys: string[],
|
||||||
|
ignoredKeys?: string[],
|
||||||
): useApiFilterReturn<F> {
|
): useApiFilterReturn<F> {
|
||||||
const [rawParams, setRawParams] = useSearchParams();
|
const [rawParams] = useSearchParams();
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const setFilter = useCallback(
|
const setFilter = useCallback(
|
||||||
(newFilter: F) => setRawParams(getStringifiedArgs(newFilter)),
|
(newFilter: F) => {
|
||||||
[setRawParams],
|
const stringifiedFilter = getStringifiedArgs(newFilter);
|
||||||
|
|
||||||
|
// If ignoredKeys is provided, ignore params that aren't managed by this hook
|
||||||
|
let updated: URLSearchParams;
|
||||||
|
if (ignoredKeys) {
|
||||||
|
updated = new URLSearchParams();
|
||||||
|
|
||||||
|
// Keep params that aren't ignored by this hook
|
||||||
|
rawParams.forEach((value, key) => {
|
||||||
|
if (!ignoredKeys.includes(key)) {
|
||||||
|
updated.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add/update managed params from the new filter
|
||||||
|
Object.entries(stringifiedFilter).forEach(([key, value]) => {
|
||||||
|
updated.set(key, value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Original behavior: replace all params
|
||||||
|
updated = new URLSearchParams(stringifiedFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use navigate to preserve the hash
|
||||||
|
const search = updated.toString();
|
||||||
|
navigate(
|
||||||
|
{
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: search ? `?${search}` : "",
|
||||||
|
hash: location.hash,
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[rawParams, ignoredKeys, navigate, location.pathname, location.hash],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filter = useMemo<F>(() => {
|
const filter = useMemo<F>(() => {
|
||||||
|
|||||||
@ -145,9 +145,13 @@ export function useHashState<S extends string>(): [
|
|||||||
const setHash = useCallback(
|
const setHash = useCallback(
|
||||||
(value: S | undefined) => {
|
(value: S | undefined) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
navigate(location.pathname);
|
navigate(location.pathname + location.search, {
|
||||||
|
state: location.state,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
navigate(`${location.pathname}#${value}`, { state: location.state });
|
navigate(`${location.pathname}${location.search}#${value}`, {
|
||||||
|
state: location.state,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import useApiFilter from "@/hooks/use-api-filter";
|
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||||
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
||||||
import { useTimezone } from "@/hooks/use-date-utils";
|
import { useTimezone } from "@/hooks/use-date-utils";
|
||||||
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
import {
|
||||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
useHashState,
|
||||||
|
useOverlayState,
|
||||||
|
useSearchEffect,
|
||||||
|
} from "@/hooks/use-overlay-state";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
import {
|
import {
|
||||||
@ -25,6 +28,7 @@ import { RecordingView } from "@/views/recording/RecordingView";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useEffect, useMemo, 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 useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
@ -37,14 +41,71 @@ export default function Events() {
|
|||||||
|
|
||||||
// recordings viewer
|
// recordings viewer
|
||||||
|
|
||||||
const [severity, setSeverity] = useOverlayState<ReviewSeverity>(
|
// showReviewed from URL params
|
||||||
"severity",
|
const [searchParams] = useSearchParams();
|
||||||
"alert",
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// severity from URL hash
|
||||||
|
const [hash, setHash] = useHashState<string>();
|
||||||
|
|
||||||
|
const severity = useMemo<ReviewSeverity>(() => {
|
||||||
|
if (hash === "motion") {
|
||||||
|
return "significant_motion";
|
||||||
|
} else if (hash === "detections") {
|
||||||
|
return "detection";
|
||||||
|
}
|
||||||
|
return "alert";
|
||||||
|
}, [hash]);
|
||||||
|
|
||||||
|
const setSeverity = useCallback(
|
||||||
|
(newSeverity: ReviewSeverity) => {
|
||||||
|
// Clicking the same tab again clears all filters (cameras, date, labels, zones, etc.)
|
||||||
|
// This provides users a quick way to reset their view
|
||||||
|
const isSameTab = newSeverity === severity;
|
||||||
|
|
||||||
|
let newHash: string;
|
||||||
|
if (newSeverity === "significant_motion") {
|
||||||
|
newHash = "motion";
|
||||||
|
} else if (newSeverity === "detection") {
|
||||||
|
newHash = "detections";
|
||||||
|
} else {
|
||||||
|
newHash = "alerts";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSameTab) {
|
||||||
|
// Clear filters by navigating without search params
|
||||||
|
navigate(`${location.pathname}#${newHash}`, { state: location.state });
|
||||||
|
} else {
|
||||||
|
setHash(newHash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[severity, setHash, navigate, location.pathname, location.state],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showReviewed, setShowReviewed] = useUserPersistence(
|
const showReviewed = useMemo(() => {
|
||||||
"showReviewed",
|
return searchParams.get("showReviewed") === "true";
|
||||||
false,
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const setShowReviewed = useCallback(
|
||||||
|
(show: boolean) => {
|
||||||
|
const updated = new URLSearchParams(searchParams);
|
||||||
|
if (show) {
|
||||||
|
updated.set("showReviewed", "true");
|
||||||
|
} else {
|
||||||
|
updated.delete("showReviewed");
|
||||||
|
}
|
||||||
|
const search = updated.toString();
|
||||||
|
navigate(
|
||||||
|
{
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: search ? `?${search}` : "",
|
||||||
|
hash: location.hash,
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[searchParams, navigate, location.pathname, location.hash],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [recording, setRecording] = useOverlayState<RecordingStartingPoint>(
|
const [recording, setRecording] = useOverlayState<RecordingStartingPoint>(
|
||||||
@ -104,31 +165,10 @@ export default function Events() {
|
|||||||
// review filter
|
// review filter
|
||||||
|
|
||||||
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
const [reviewFilter, setReviewFilter, reviewSearchParams] =
|
||||||
useApiFilter<ReviewFilter>();
|
useApiFilterArgs<ReviewFilter>(
|
||||||
|
["cameras", "labels", "zones"],
|
||||||
useSearchEffect("cameras", (cameras: string) => {
|
["cameras", "labels", "zones", "before", "after", "showAll"],
|
||||||
setReviewFilter({
|
);
|
||||||
...reviewFilter,
|
|
||||||
cameras: cameras.includes(",") ? cameras.split(",") : [cameras],
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
useSearchEffect("labels", (labels: string) => {
|
|
||||||
setReviewFilter({
|
|
||||||
...reviewFilter,
|
|
||||||
labels: labels.includes(",") ? labels.split(",") : [labels],
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
useSearchEffect("zones", (zones: string) => {
|
|
||||||
setReviewFilter({
|
|
||||||
...reviewFilter,
|
|
||||||
zones: zones.includes(",") ? zones.split(",") : [zones],
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
useSearchEffect("group", (reviewGroup) => {
|
useSearchEffect("group", (reviewGroup) => {
|
||||||
if (config && reviewGroup && reviewGroup != "default") {
|
if (config && reviewGroup && reviewGroup != "default") {
|
||||||
|
|||||||
@ -291,8 +291,9 @@ export default function EventView({
|
|||||||
size="sm"
|
size="sm"
|
||||||
value={severityToggle}
|
value={severityToggle}
|
||||||
onValueChange={(value: ReviewSeverity) =>
|
onValueChange={(value: ReviewSeverity) =>
|
||||||
value ? setSeverityToggle(value) : null
|
// If the user clicks the same tab twice, clear filters as a handy shortcut
|
||||||
} // don't allow the severity to be unselected
|
setSeverityToggle(value || severityToggle)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={cn(severityToggle != "alert" && "text-muted-foreground")}
|
className={cn(severityToggle != "alert" && "text-muted-foreground")}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user