mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-26 18:18:22 +03:00
Various Tweaks (#22609)
Some checks are pending
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
Some checks are pending
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:
parent
334245bd3c
commit
4b42039568
@ -2,6 +2,7 @@ import unittest
|
||||
|
||||
from frigate.api.auth import resolve_role
|
||||
from frigate.config import HeaderMappingConfig, ProxyConfig
|
||||
from frigate.config.env import FRIGATE_ENV_VARS
|
||||
|
||||
|
||||
class TestProxyRoleResolution(unittest.TestCase):
|
||||
@ -91,3 +92,39 @@ class TestProxyRoleResolution(unittest.TestCase):
|
||||
headers = {"x-remote-role": "group_unknown"}
|
||||
role = resolve_role(headers, self.proxy_config, self.config_roles)
|
||||
self.assertEqual(role, self.proxy_config.default_role)
|
||||
|
||||
|
||||
class TestProxyAuthSecretEnvString(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._original_env_vars = dict(FRIGATE_ENV_VARS)
|
||||
|
||||
def tearDown(self):
|
||||
FRIGATE_ENV_VARS.clear()
|
||||
FRIGATE_ENV_VARS.update(self._original_env_vars)
|
||||
|
||||
def test_auth_secret_env_substitution(self):
|
||||
"""auth_secret resolves FRIGATE_ env vars via EnvString."""
|
||||
FRIGATE_ENV_VARS["FRIGATE_PROXY_SECRET"] = "my_secret_value"
|
||||
config = ProxyConfig(auth_secret="{FRIGATE_PROXY_SECRET}")
|
||||
self.assertEqual(config.auth_secret, "my_secret_value")
|
||||
|
||||
def test_auth_secret_env_embedded_in_string(self):
|
||||
"""auth_secret resolves env vars embedded in a larger string."""
|
||||
FRIGATE_ENV_VARS["FRIGATE_SECRET_PART"] = "abc123"
|
||||
config = ProxyConfig(auth_secret="prefix-{FRIGATE_SECRET_PART}-suffix")
|
||||
self.assertEqual(config.auth_secret, "prefix-abc123-suffix")
|
||||
|
||||
def test_auth_secret_plain_string(self):
|
||||
"""auth_secret accepts a plain string without substitution."""
|
||||
config = ProxyConfig(auth_secret="literal_secret")
|
||||
self.assertEqual(config.auth_secret, "literal_secret")
|
||||
|
||||
def test_auth_secret_none(self):
|
||||
"""auth_secret defaults to None."""
|
||||
config = ProxyConfig()
|
||||
self.assertIsNone(config.auth_secret)
|
||||
|
||||
def test_auth_secret_unknown_var_raises(self):
|
||||
"""auth_secret raises KeyError for unknown env var references."""
|
||||
with self.assertRaises(Exception):
|
||||
ProxyConfig(auth_secret="{FRIGATE_NONEXISTENT_VAR}")
|
||||
|
||||
@ -271,18 +271,17 @@ def get_min_region_size(model_config: ModelConfig) -> int:
|
||||
"""Get the min region size."""
|
||||
largest_dimension = max(model_config.height, model_config.width)
|
||||
|
||||
if largest_dimension > 320:
|
||||
# We originally tested allowing any model to have a region down to half of the model size
|
||||
# but this led to many false positives. In this case we specifically target larger models
|
||||
# which can benefit from a smaller region in some cases to detect smaller objects.
|
||||
half = int(largest_dimension / 2)
|
||||
# return largest dimension for smaller models, but make sure the dimension is normalized
|
||||
if largest_dimension < 320:
|
||||
if largest_dimension % 4 == 0:
|
||||
return largest_dimension
|
||||
|
||||
if half % 4 == 0:
|
||||
return half
|
||||
return int((largest_dimension + 3) / 4) * 4
|
||||
|
||||
return int((half + 3) / 4) * 4
|
||||
|
||||
return largest_dimension
|
||||
# Any model that is 320 or larger should have a minimum region size of 320
|
||||
# this allows larger models to use smaller regions to detect smaller objects
|
||||
# in the case that the motion area is smaller so that it can be upscaled.
|
||||
return 320
|
||||
|
||||
|
||||
def create_tensor_input(frame, model_config: ModelConfig, region):
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/images/branding/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Frigate</title>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -40,6 +40,8 @@ module.exports = {
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"collapsible-down": "collapsible-down 0.2s ease-out",
|
||||
"collapsible-up": "collapsible-up 0.2s ease-out",
|
||||
move: "move 3s ease-in-out infinite",
|
||||
scale1: "scale1 3s ease-in-out infinite",
|
||||
scale2: "scale2 3s ease-in-out infinite",
|
||||
@ -148,6 +150,14 @@ module.exports = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"collapsible-down": {
|
||||
from: { height: "0px" },
|
||||
to: { height: "var(--radix-collapsible-content-height)" },
|
||||
},
|
||||
"collapsible-up": {
|
||||
from: { height: "var(--radix-collapsible-content-height)" },
|
||||
to: { height: "0px" },
|
||||
},
|
||||
move: {
|
||||
"50%": { left: "calc(100% - 7px)" },
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user