mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 17:12:16 +03:00
Various Tweaks (#22609)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Mark items as reviewed as a group with keyboard * Improve handling of half model regions * update viewport meta tag to prevent user scaling fixes https://github.com/blakeblackshear/frigate/issues/22017 * add small animation to collapsible shadcn elements * add proxy auth env var tests * Improve search effect * Fix mobile back navigation losing overlay state on classification page * undo historyBack changes * fix classification history navigation --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
co-authored by
Josh Hawkins
parent
334245bd3c
commit
4b42039568
@@ -6,7 +6,7 @@ import {
|
||||
ClassificationThreshold,
|
||||
ClassifiedEvent,
|
||||
} from "@/types/classification";
|
||||
import { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { isDesktop, isIOS, isMobile, isMobileOnly } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TimeAgo from "../dynamic/TimeAgo";
|
||||
@@ -216,6 +216,25 @@ export function GroupedClassificationCard({
|
||||
const { t } = useTranslation(["views/explore", i18nLibrary]);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
|
||||
// If the component unmounts while the detail overlay is open, we need to
|
||||
// pop the history state that was pushed by useHistoryBack, otherwise it
|
||||
// leaves a stale entry that breaks back navigation.
|
||||
const detailOpenRef = useRef(detailOpen);
|
||||
useEffect(() => {
|
||||
detailOpenRef.current = detailOpen;
|
||||
}, [detailOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Only pop the state if we are still sitting on the overlayOpen history entry.
|
||||
// This prevents the unmount from undoing cross-page routing if the unmount
|
||||
// was caused by navigating away to a different view.
|
||||
if (detailOpenRef.current && window.history.state?.overlayOpen) {
|
||||
window.history.back();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// data
|
||||
|
||||
const bestItem = useMemo<ClassificationItemData | undefined>(() => {
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
import * as React from "react";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
const CollapsibleContent = React.forwardRef<
|
||||
React.ComponentRef<typeof CollapsiblePrimitive.CollapsibleContent>,
|
||||
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</CollapsiblePrimitive.CollapsibleContent>
|
||||
));
|
||||
CollapsibleContent.displayName =
|
||||
CollapsiblePrimitive.CollapsibleContent.displayName;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { usePersistence } from "./use-persistence";
|
||||
import { useUserPersistence } from "./use-user-persistence";
|
||||
@@ -200,36 +207,51 @@ export function useSearchEffect(
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [pendingRemoval, setPendingRemoval] = useState(false);
|
||||
const processedRef = useRef<string | null>(null);
|
||||
|
||||
const param = useMemo(() => {
|
||||
const param = searchParams.get(key);
|
||||
|
||||
if (!param) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [key, decodeURIComponent(param)];
|
||||
}, [searchParams, key]);
|
||||
const currentParam = searchParams.get(key);
|
||||
|
||||
// Process the param via callback (once per unique param value)
|
||||
useEffect(() => {
|
||||
if (!param) {
|
||||
if (currentParam == null || currentParam === processedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remove = callback(param[1]);
|
||||
const decoded = decodeURIComponent(currentParam);
|
||||
const shouldRemove = callback(decoded);
|
||||
|
||||
if (remove) {
|
||||
navigate(location.pathname + location.hash, {
|
||||
state: location.state,
|
||||
replace: true,
|
||||
});
|
||||
if (shouldRemove) {
|
||||
processedRef.current = currentParam;
|
||||
setPendingRemoval(true);
|
||||
}
|
||||
}, [currentParam, callback, key]);
|
||||
|
||||
// Remove the search param in a separate render cycle so that any state
|
||||
// changes from the callback (e.g., overlay state navigations) are already
|
||||
// reflected in location.state before we navigate to strip the param.
|
||||
useEffect(() => {
|
||||
if (!pendingRemoval) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingRemoval(false);
|
||||
navigate(location.pathname + location.hash, {
|
||||
state: location.state,
|
||||
replace: true,
|
||||
});
|
||||
}, [
|
||||
param,
|
||||
location.state,
|
||||
pendingRemoval,
|
||||
navigate,
|
||||
location.pathname,
|
||||
location.hash,
|
||||
callback,
|
||||
navigate,
|
||||
location.state,
|
||||
]);
|
||||
|
||||
// Reset tracking when param is removed from the URL
|
||||
useEffect(() => {
|
||||
if (currentParam == null) {
|
||||
processedRef.current = null;
|
||||
}
|
||||
}, [currentParam]);
|
||||
}
|
||||
|
||||
@@ -409,8 +409,11 @@ export default function Events() {
|
||||
|
||||
// review status
|
||||
|
||||
const markAllItemsAsReviewed = useCallback(
|
||||
async (currentItems: ReviewSegment[]) => {
|
||||
const markItemsAsReviewed = useCallback(
|
||||
async (
|
||||
currentItems: ReviewSegment[],
|
||||
itemsToMarkReviewed: ReviewSegment[] | undefined = undefined,
|
||||
) => {
|
||||
if (currentItems.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -435,13 +438,14 @@ export default function Events() {
|
||||
{ revalidate: false, populateCache: true },
|
||||
);
|
||||
|
||||
const itemsToMarkReviewed = currentItems
|
||||
?.filter((seg) => seg.end_time)
|
||||
?.map((seg) => seg.id);
|
||||
const reviewList =
|
||||
itemsToMarkReviewed?.map((seg) => seg.id) ||
|
||||
currentItems?.filter((seg) => seg.end_time)?.map((seg) => seg.id) ||
|
||||
[];
|
||||
|
||||
if (itemsToMarkReviewed.length > 0) {
|
||||
if (reviewList.length > 0) {
|
||||
await axios.post(`reviews/viewed`, {
|
||||
ids: itemsToMarkReviewed,
|
||||
ids: reviewList,
|
||||
reviewed: true,
|
||||
});
|
||||
reloadData();
|
||||
@@ -611,7 +615,7 @@ export default function Events() {
|
||||
setShowReviewed={setShowReviewed}
|
||||
setSeverity={setSeverity}
|
||||
markItemAsReviewed={markItemAsReviewed}
|
||||
markAllItemsAsReviewed={markAllItemsAsReviewed}
|
||||
markItemsAsReviewed={markItemsAsReviewed}
|
||||
onOpenRecording={setRecording}
|
||||
motionPreviewsCamera={motionPreviewsCamera ?? null}
|
||||
setMotionPreviewsCamera={(camera) =>
|
||||
|
||||
@@ -110,7 +110,10 @@ type EventViewProps = {
|
||||
setShowReviewed: (show: boolean) => void;
|
||||
setSeverity: (severity: ReviewSeverity) => void;
|
||||
markItemAsReviewed: (review: ReviewSegment) => void;
|
||||
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
||||
markItemsAsReviewed: (
|
||||
currentItems: ReviewSegment[],
|
||||
itemsToMarkReviewed?: ReviewSegment[] | undefined,
|
||||
) => void;
|
||||
onOpenRecording: (recordingInfo: RecordingStartingPoint) => void;
|
||||
motionPreviewsCamera: string | null;
|
||||
setMotionPreviewsCamera: (camera: string | null) => void;
|
||||
@@ -132,7 +135,7 @@ export default function EventView({
|
||||
setShowReviewed,
|
||||
setSeverity,
|
||||
markItemAsReviewed,
|
||||
markAllItemsAsReviewed,
|
||||
markItemsAsReviewed,
|
||||
onOpenRecording,
|
||||
motionPreviewsCamera,
|
||||
setMotionPreviewsCamera,
|
||||
@@ -498,7 +501,7 @@ export default function EventView({
|
||||
loading={severity != severityToggle}
|
||||
emptyCardData={emptyCardData}
|
||||
markItemAsReviewed={markItemAsReviewed}
|
||||
markAllItemsAsReviewed={markAllItemsAsReviewed}
|
||||
markItemsAsReviewed={markItemsAsReviewed}
|
||||
onSelectReview={onSelectReview}
|
||||
onSelectAllReviews={onSelectAllReviews}
|
||||
setSelectedReviews={setSelectedReviews}
|
||||
@@ -549,7 +552,10 @@ type DetectionReviewProps = {
|
||||
loading: boolean;
|
||||
emptyCardData: EmptyCardData;
|
||||
markItemAsReviewed: (review: ReviewSegment) => void;
|
||||
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
||||
markItemsAsReviewed: (
|
||||
currentItems: ReviewSegment[],
|
||||
itemsToMarkReviewed?: ReviewSegment[] | undefined,
|
||||
) => void;
|
||||
onSelectReview: (
|
||||
review: ReviewSegment,
|
||||
ctrl: boolean,
|
||||
@@ -573,7 +579,7 @@ function DetectionReview({
|
||||
loading,
|
||||
emptyCardData,
|
||||
markItemAsReviewed,
|
||||
markAllItemsAsReviewed,
|
||||
markItemsAsReviewed,
|
||||
onSelectReview,
|
||||
onSelectAllReviews,
|
||||
setSelectedReviews,
|
||||
@@ -788,12 +794,7 @@ function DetectionReview({
|
||||
break;
|
||||
case "r":
|
||||
if (selectedReviews.length > 0 && !modifiers.repeat) {
|
||||
currentItems?.forEach((item) => {
|
||||
if (selectedReviews.some((r) => r.id === item.id)) {
|
||||
item.has_been_reviewed = true;
|
||||
markItemAsReviewed(item);
|
||||
}
|
||||
});
|
||||
markItemsAsReviewed(currentItems || [], selectedReviews);
|
||||
setSelectedReviews([]);
|
||||
return true;
|
||||
}
|
||||
@@ -903,7 +904,7 @@ function DetectionReview({
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
setSelectedReviews([]);
|
||||
markAllItemsAsReviewed(currentItems ?? []);
|
||||
markItemsAsReviewed(currentItems ?? []);
|
||||
}}
|
||||
>
|
||||
{t("markTheseItemsAsReviewed")}
|
||||
|
||||
Reference in New Issue
Block a user