setImageLoaded(true)}
+ ref={(node) => {
+ if (node?.complete && node.naturalWidth > 0) {
+ setImageLoaded(true);
+ }
+ }}
/>
{!imageLoaded && (
diff --git a/web/src/views/motion-search/MotionSearchROICanvas.tsx b/web/src/views/motion-search/MotionSearchROICanvas.tsx
index 43fac7a48..b0d90da81 100644
--- a/web/src/views/motion-search/MotionSearchROICanvas.tsx
+++ b/web/src/views/motion-search/MotionSearchROICanvas.tsx
@@ -1,10 +1,9 @@
-import { useCallback, useMemo, useRef } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Stage, Layer, Line, Circle, Image } from "react-konva";
import Konva from "konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { flattenPoints } from "@/utils/canvasUtil";
import { cn } from "@/lib/utils";
-import { useResizeObserver } from "@/hooks/resize-observer";
type MotionSearchROICanvasProps = {
camera: string;
@@ -30,18 +29,40 @@ export default function MotionSearchROICanvas({
motionHeatmap,
showMotionHeatmap = false,
}: MotionSearchROICanvasProps) {
- const containerRef = useRef
(null);
const stageRef = useRef(null);
- const [{ width: containerWidth, height: containerHeight }] =
- useResizeObserver(containerRef);
-
- const stageSize = useMemo(
- () => ({
- width: containerWidth > 0 ? Math.ceil(containerWidth) : 0,
- height: containerHeight > 0 ? Math.ceil(containerHeight) : 0,
- }),
- [containerHeight, containerWidth],
+ const [containerNode, setContainerNode] = useState(
+ null,
);
+ const [stageSize, setStageSize] = useState({ width: 0, height: 0 });
+
+ useEffect(() => {
+ if (!containerNode) {
+ return;
+ }
+
+ const apply = (width: number, height: number) => {
+ setStageSize((prev) => {
+ const next = {
+ width: width > 0 ? Math.ceil(width) : 0,
+ height: height > 0 ? Math.ceil(height) : 0,
+ };
+ if (prev.width === next.width && prev.height === next.height) {
+ return prev;
+ }
+ return next;
+ });
+ };
+
+ apply(containerNode.clientWidth, containerNode.clientHeight);
+
+ const observer = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (!entry) return;
+ apply(entry.contentRect.width, entry.contentRect.height);
+ });
+ observer.observe(containerNode);
+ return () => observer.disconnect();
+ }, [containerNode]);
const videoRect = useMemo(() => {
const stageWidth = stageSize.width;
@@ -317,7 +338,7 @@ export default function MotionSearchROICanvas({
return (
e.evt.stopPropagation()}
+ onTouchStart={(e) => e.evt.stopPropagation()}
onDragMove={(e) => handlePointDragMove(e, index)}
onMouseOver={(e) => handleMouseOverPoint(e, index)}
onMouseOut={(e) => handleMouseOutPoint(e, index)}
diff --git a/web/src/views/motion-search/MotionSearchView.tsx b/web/src/views/motion-search/MotionSearchView.tsx
index 961c1065c..34c52cc5d 100644
--- a/web/src/views/motion-search/MotionSearchView.tsx
+++ b/web/src/views/motion-search/MotionSearchView.tsx
@@ -994,15 +994,20 @@ export default function MotionSearchView({
);
const progressMetrics = jobStatus?.metrics ?? searchMetrics;
- const progressValue =
- progressMetrics && progressMetrics.segments_scanned > 0
- ? Math.min(
- 100,
- (progressMetrics.segments_processed /
- progressMetrics.segments_scanned) *
- 100,
- )
- : 0;
+ const progressValue = (() => {
+ if (!progressMetrics || progressMetrics.segments_scanned <= 0) {
+ return 0;
+ }
+ const skipped =
+ progressMetrics.heatmap_roi_skip_segments +
+ progressMetrics.metadata_inactive_segments;
+ const totalWork = progressMetrics.segments_scanned - skipped;
+ const doneWork = progressMetrics.segments_processed - skipped;
+ if (totalWork <= 0) {
+ return 100;
+ }
+ return Math.min(100, Math.max(0, (doneWork / totalWork) * 100));
+ })();
const resultsPanel = (
<>
@@ -1036,8 +1041,8 @@ export default function MotionSearchView({
)}
- {searchMetrics && searchResults.length > 0 && (
-
+ {searchMetrics && (isSearching || searchResults.length > 0) && (
+
{t("metrics.segmentsScanned")}
diff --git a/web/src/views/settings/ObjectSettingsView.tsx b/web/src/views/settings/ObjectSettingsView.tsx
index 2f4ec5eaf..3179fb85c 100644
--- a/web/src/views/settings/ObjectSettingsView.tsx
+++ b/web/src/views/settings/ObjectSettingsView.tsx
@@ -370,7 +370,7 @@ type ObjectListProps = {
};
function ObjectList({ cameraConfig, objects }: ObjectListProps) {
- const { t } = useTranslation(["views/settings"]);
+ const { t } = useTranslation(["views/settings", "common"]);
const { data: config } = useSWR
("config");
const colormap = useMemo(() => {
@@ -440,17 +440,21 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
{obj.area ? (
- px: {obj.area.toString()}
+ {t("information.pixels", {
+ ns: "common",
+ area: obj.area,
+ })}
- %:{" "}
{(
- obj.area /
- (cameraConfig.detect.width *
- cameraConfig.detect.height)
+ (obj.area /
+ (cameraConfig.detect.width *
+ cameraConfig.detect.height)) *
+ 100
)
- .toFixed(4)
+ .toFixed(2)
.toString()}
+ %
) : (
diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx
index 00855b948..df7287fad 100644
--- a/web/src/views/system/StorageMetrics.tsx
+++ b/web/src/views/system/StorageMetrics.tsx
@@ -22,6 +22,7 @@ import { Link } from "react-router-dom";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { LuExternalLink } from "react-icons/lu";
import { FaExclamationTriangle } from "react-icons/fa";
+import ActivityIndicator from "@/components/indicators/activity-indicator";
type CameraStorage = {
[key: string]: {
@@ -128,7 +129,11 @@ export default function StorageMetrics({
}, [stats, config]);
if (!cameraStorage || !stats || !totalStorage || !config) {
- return;
+ return (
+
+ );
}
return (