mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-21 00:06:44 +01:00
Improve navigation (#13833)
* Fix infinite loop * Fix review page not opening to historical review items * Use query arg for search and remove unused recording opening * Retain query * Clean up typing
This commit is contained in:
parent
7c63cb5bca
commit
27e71eb142
@ -189,6 +189,8 @@ export default function InputWithTags({
|
||||
let timestamp = 0;
|
||||
|
||||
switch (type) {
|
||||
case "query":
|
||||
break;
|
||||
case "before":
|
||||
case "after":
|
||||
timestamp = convertLocalDateToTimestamp(value);
|
||||
@ -584,24 +586,26 @@ export default function InputWithTags({
|
||||
)}
|
||||
{Object.entries(filters).map(([filterType, filterValues]) =>
|
||||
Array.isArray(filterValues) ? (
|
||||
filterValues.map((value, index) => (
|
||||
<span
|
||||
key={`${filterType}-${index}`}
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||
>
|
||||
{filterType.replaceAll("_", " ")}:{" "}
|
||||
{value.replaceAll("_", " ")}
|
||||
<button
|
||||
onClick={() =>
|
||||
removeFilter(filterType as FilterType, value)
|
||||
}
|
||||
className="ml-1 focus:outline-none"
|
||||
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
|
||||
filterValues
|
||||
.filter(() => filterType !== "query")
|
||||
.map((value, index) => (
|
||||
<span
|
||||
key={`${filterType}-${index}`}
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||
>
|
||||
<LuX className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
{filterType.replaceAll("_", " ")}:{" "}
|
||||
{value.replaceAll("_", " ")}
|
||||
<button
|
||||
onClick={() =>
|
||||
removeFilter(filterType as FilterType, value)
|
||||
}
|
||||
className="ml-1 focus:outline-none"
|
||||
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
|
||||
>
|
||||
<LuX className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span
|
||||
key={filterType}
|
||||
|
@ -125,7 +125,7 @@ export function useSearchEffect(
|
||||
const remove = callback(param[1]);
|
||||
|
||||
if (remove) {
|
||||
setSearchParams();
|
||||
setSearchParams(undefined, { replace: true });
|
||||
}
|
||||
}, [param, callback, setSearchParams]);
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ import {
|
||||
ReviewSummary,
|
||||
SegmentedReviewData,
|
||||
} from "@/types/review";
|
||||
import {
|
||||
getBeginningOfDayTimestamp,
|
||||
getEndOfDayTimestamp,
|
||||
} from "@/utils/dateUtil";
|
||||
import EventView from "@/views/events/EventView";
|
||||
import { RecordingView } from "@/views/recording/RecordingView";
|
||||
import axios from "axios";
|
||||
@ -43,10 +47,17 @@ export default function Events() {
|
||||
.get(`review/${reviewId}`)
|
||||
.then((resp) => {
|
||||
if (resp.status == 200 && resp.data) {
|
||||
const startTime = resp.data.start_time - REVIEW_PADDING;
|
||||
const date = new Date(startTime * 1000);
|
||||
|
||||
setReviewFilter({
|
||||
after: getBeginningOfDayTimestamp(date),
|
||||
before: getEndOfDayTimestamp(date),
|
||||
});
|
||||
setRecording(
|
||||
{
|
||||
camera: resp.data.camera,
|
||||
startTime: resp.data.start_time - REVIEW_PADDING,
|
||||
startTime,
|
||||
severity: resp.data.severity,
|
||||
},
|
||||
true,
|
||||
|
@ -1,30 +1,24 @@
|
||||
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
||||
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { RecordingStartingPoint } from "@/types/record";
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { RecordingView } from "@/views/recording/RecordingView";
|
||||
import SearchView from "@/views/search/SearchView";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
|
||||
const API_LIMIT = 25;
|
||||
|
||||
export default function Explore() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
// search field handler
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const [recording, setRecording] =
|
||||
useOverlayState<RecordingStartingPoint>("recording");
|
||||
const [searchFilter, setSearchFilter, searchSearchParams] =
|
||||
useApiFilterArgs<SearchFilter>();
|
||||
|
||||
const searchTerm = useMemo(
|
||||
() => searchSearchParams?.["query"] || "",
|
||||
[searchSearchParams],
|
||||
);
|
||||
|
||||
// search filter
|
||||
|
||||
@ -36,11 +30,13 @@ export default function Explore() {
|
||||
return searchTerm.split(":")[1];
|
||||
}, [searchTerm]);
|
||||
|
||||
const [searchFilter, setSearchFilter, searchSearchParams] =
|
||||
useApiFilterArgs<SearchFilter>();
|
||||
|
||||
// search api
|
||||
|
||||
useSearchEffect("query", (query) => {
|
||||
setSearch(query);
|
||||
return false;
|
||||
});
|
||||
|
||||
useSearchEffect("similarity_search_id", (similarityId) => {
|
||||
setSearch(`similarity:${similarityId}`);
|
||||
// @ts-expect-error we want to clear this
|
||||
@ -49,7 +45,16 @@ export default function Explore() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(search);
|
||||
if (!searchTerm && !search) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchFilter({
|
||||
...searchFilter,
|
||||
query: search.length > 0 ? search : undefined,
|
||||
});
|
||||
// only update when search is updated
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [search]);
|
||||
|
||||
const searchQuery: SearchQuery = useMemo(() => {
|
||||
@ -168,109 +173,19 @@ export default function Explore() {
|
||||
}
|
||||
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
|
||||
|
||||
// previews
|
||||
|
||||
const previewTimeRange = useMemo<TimeRange>(() => {
|
||||
if (!searchResults) {
|
||||
return { after: 0, before: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
after: Math.min(...searchResults.map((res) => res.start_time)),
|
||||
before: Math.max(
|
||||
...searchResults.map((res) => res.end_time ?? Date.now() / 1000),
|
||||
),
|
||||
};
|
||||
}, [searchResults]);
|
||||
|
||||
const allPreviews = useCameraPreviews(previewTimeRange, {
|
||||
autoRefresh: false,
|
||||
fetchPreviews: searchResults != undefined,
|
||||
});
|
||||
|
||||
// selection
|
||||
|
||||
const onOpenSearch = useCallback(
|
||||
(item: SearchResult) => {
|
||||
setRecording({
|
||||
camera: item.camera,
|
||||
startTime: item.start_time,
|
||||
severity: "alert",
|
||||
});
|
||||
},
|
||||
[setRecording],
|
||||
return (
|
||||
<SearchView
|
||||
search={search}
|
||||
searchTerm={searchTerm}
|
||||
searchFilter={searchFilter}
|
||||
searchResults={searchResults}
|
||||
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||
setSearch={setSearch}
|
||||
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
||||
setSearchFilter={setSearchFilter}
|
||||
onUpdateFilter={setSearchFilter}
|
||||
loadMore={loadMore}
|
||||
hasMore={!isReachingEnd}
|
||||
/>
|
||||
);
|
||||
|
||||
const selectedReviewData = useMemo(() => {
|
||||
if (!recording) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!searchResults) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allCameras = searchFilter?.cameras ?? Object.keys(config.cameras);
|
||||
|
||||
return {
|
||||
camera: recording.camera,
|
||||
start_time: recording.startTime,
|
||||
allCameras: allCameras,
|
||||
};
|
||||
|
||||
// previews will not update after item is selected
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [recording, searchResults]);
|
||||
|
||||
const selectedTimeRange = useMemo(() => {
|
||||
if (!recording) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const time = new Date(recording.startTime * 1000);
|
||||
time.setUTCMinutes(0, 0, 0);
|
||||
const start = time.getTime() / 1000;
|
||||
time.setHours(time.getHours() + 2);
|
||||
const end = time.getTime() / 1000;
|
||||
return {
|
||||
after: start,
|
||||
before: end,
|
||||
};
|
||||
}, [recording]);
|
||||
|
||||
if (recording) {
|
||||
if (selectedReviewData && selectedTimeRange) {
|
||||
return (
|
||||
<RecordingView
|
||||
startCamera={selectedReviewData.camera}
|
||||
startTime={selectedReviewData.start_time}
|
||||
allCameras={selectedReviewData.allCameras}
|
||||
allPreviews={allPreviews}
|
||||
timeRange={selectedTimeRange}
|
||||
updateFilter={setSearchFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<SearchView
|
||||
search={search}
|
||||
searchTerm={searchTerm}
|
||||
searchFilter={searchFilter}
|
||||
searchResults={searchResults}
|
||||
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||
setSearch={setSearch}
|
||||
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
||||
setSearchFilter={setSearchFilter}
|
||||
onUpdateFilter={setSearchFilter}
|
||||
onOpenSearch={onOpenSearch}
|
||||
loadMore={loadMore}
|
||||
hasMore={!isReachingEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export type SearchResult = {
|
||||
};
|
||||
|
||||
export type SearchFilter = {
|
||||
query?: string;
|
||||
cameras?: string[];
|
||||
labels?: string[];
|
||||
subLabels?: string[];
|
||||
|
@ -285,6 +285,11 @@ export function endOfHourOrCurrentTime(timestamp: number) {
|
||||
return Math.min(timestamp, now.getTime() / 1000);
|
||||
}
|
||||
|
||||
export function getBeginningOfDayTimestamp(date: Date) {
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date.getTime() / 1000;
|
||||
}
|
||||
|
||||
export function getEndOfDayTimestamp(date: Date) {
|
||||
date.setHours(23, 59, 59, 999);
|
||||
return date.getTime() / 1000;
|
||||
|
@ -33,7 +33,6 @@ type SearchViewProps = {
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
setSearchFilter: (filter: SearchFilter) => void;
|
||||
onUpdateFilter: (filter: SearchFilter) => void;
|
||||
onOpenSearch: (item: SearchResult) => void;
|
||||
loadMore: () => void;
|
||||
hasMore: boolean;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user