From 485057abc1528ca9a4f2eb1744ba2d8252eb22e2 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 28 Feb 2024 07:18:08 -0600
Subject: [PATCH 1/3] Adapt review timeline for mobile devices (#10120)
* adapt timeline to mobile
* remove unused
* tweaks
* pointer cursor on segments
---
web/package-lock.json | 32 +++++
web/package.json | 1 +
web/src/components/player/LivePlayer.tsx | 4 +-
.../timeline/EventReviewTimeline.tsx | 72 +++++-----
web/src/components/timeline/EventSegment.tsx | 125 +++++++++---------
web/src/components/ui/hover-card.tsx | 27 ++++
web/src/hooks/use-event-utils.ts | 14 +-
web/src/hooks/use-handle-dragging.ts | 7 +-
web/src/views/events/EventView.tsx | 7 +-
9 files changed, 183 insertions(+), 106 deletions(-)
create mode 100644 web/src/components/ui/hover-card.tsx
diff --git a/web/package-lock.json b/web/package-lock.json
index 34b159fa1..5b279adc3 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -15,6 +15,7 @@
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -1392,6 +1393,37 @@
}
}
},
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz",
+ "integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-popper": "1.1.3",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-controllable-state": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
diff --git a/web/package.json b/web/package.json
index ebdb6dceb..8eae592c4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx
index 8efb7f972..5eede4575 100644
--- a/web/src/components/player/LivePlayer.tsx
+++ b/web/src/components/player/LivePlayer.tsx
@@ -127,8 +127,8 @@ export default function LivePlayer({
diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx
index 428a9ec37..6637dda3d 100644
--- a/web/src/components/timeline/EventReviewTimeline.tsx
+++ b/web/src/components/timeline/EventReviewTimeline.tsx
@@ -10,7 +10,6 @@ import {
import EventSegment from "./EventSegment";
import { useEventUtils } from "@/hooks/use-event-utils";
import { ReviewSegment, ReviewSeverity } from "@/types/review";
-import { TooltipProvider } from "../ui/tooltip";
export type EventReviewTimelineProps = {
segmentDuration: number;
@@ -56,14 +55,18 @@ export function EventReviewTimeline({
[timelineEnd, timelineStart]
);
- const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
+ const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
+ events,
+ segmentDuration
+ );
const { handleMouseDown, handleMouseUp, handleMouseMove } =
useDraggableHandler({
contentRef,
timelineRef,
scrollTimeRef,
- alignDateToTimeline,
+ alignStartDateToTimeline,
+ alignEndDateToTimeline,
segmentDuration,
showHandlebar,
timelineDuration,
@@ -96,7 +99,7 @@ export function EventReviewTimeline({
// Generate segments for the timeline
const generateSegments = useCallback(() => {
const segmentCount = timelineDuration / segmentDuration;
- const segmentAlignedTime = alignDateToTimeline(timelineStart);
+ const segmentAlignedTime = alignStartDateToTimeline(timelineStart);
return Array.from({ length: segmentCount }, (_, index) => {
const segmentTime = segmentAlignedTime - index * segmentDuration;
@@ -172,7 +175,7 @@ export function EventReviewTimeline({
timelineHeight / (timelineDuration / segmentDuration);
// Calculate the segment index corresponding to the target time
- const alignedHandlebarTime = alignDateToTimeline(handlebarTime);
+ const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime);
const segmentIndex = Math.ceil(
(timelineStart - alignedHandlebarTime) / segmentDuration
);
@@ -213,44 +216,39 @@ export function EventReviewTimeline({
]);
return (
-
-
-
{segments}
- {showHandlebar && (
-
-
+
+
{segments}
+ {showHandlebar && (
+
+
+
-
+ ref={currentTimeRef}
+ className="text-white text-xs z-10"
+ >
+
- )}
-
-
+
+ )}
+
);
}
diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx
index d3b249688..78d54d578 100644
--- a/web/src/components/timeline/EventSegment.tsx
+++ b/web/src/components/timeline/EventSegment.tsx
@@ -9,8 +9,13 @@ import React, {
useMemo,
useRef,
} from "react";
-import { Tooltip, TooltipContent } from "../ui/tooltip";
-import { TooltipTrigger } from "@radix-ui/react-tooltip";
+import { isDesktop } from "react-device-detect";
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+} from "../ui/hover-card";
+import { HoverCardPortal } from "@radix-ui/react-hover-card";
type EventSegmentProps = {
events: ReviewSegment[];
@@ -33,8 +38,6 @@ type MinimapSegmentProps = {
};
type TickSegmentProps = {
- isFirstSegmentInMinimap: boolean;
- isLastSegmentInMinimap: boolean;
timestamp: Date;
timestampSpread: number;
};
@@ -58,25 +61,23 @@ function MinimapBounds({
<>
{isFirstSegmentInMinimap && (
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
- month: "short",
- day: "2-digit",
+ ...(isDesktop && { month: "short", day: "2-digit" }),
})}
)}
{isLastSegmentInMinimap && (
-
+
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
- month: "short",
- day: "2-digit",
+ ...(isDesktop && { month: "short", day: "2-digit" }),
})}
)}
@@ -84,15 +85,10 @@ function MinimapBounds({
);
}
-function Tick({
- isFirstSegmentInMinimap,
- isLastSegmentInMinimap,
- timestamp,
- timestampSpread,
-}: TickSegmentProps) {
+function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
return (
-
- {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
+
);
}
@@ -114,7 +110,7 @@ function Timestamp({
segmentKey,
}: TimestampSegmentProps) {
return (
-
+
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
getSeverity(segmentTime, displaySeverityType),
@@ -177,7 +176,7 @@ export function EventSegment({
const startTimestamp = useMemo(() => {
const eventStart = getEventStart(segmentTime);
if (eventStart) {
- return alignDateToTimeline(eventStart);
+ return alignStartDateToTimeline(eventStart);
}
}, [getEventStart, segmentTime]);
@@ -191,23 +190,26 @@ export function EventSegment({
const segmentKey = useMemo(() => segmentTime, [segmentTime]);
const alignedMinimapStartTime = useMemo(
- () => alignDateToTimeline(minimapStartTime ?? 0),
- [minimapStartTime, alignDateToTimeline]
+ () => alignStartDateToTimeline(minimapStartTime ?? 0),
+ [minimapStartTime, alignStartDateToTimeline]
);
const alignedMinimapEndTime = useMemo(
- () => alignDateToTimeline(minimapEndTime ?? 0),
- [minimapEndTime, alignDateToTimeline]
+ () => alignEndDateToTimeline(minimapEndTime ?? 0),
+ [minimapEndTime, alignEndDateToTimeline]
);
const isInMinimapRange = useMemo(() => {
return (
showMinimap &&
- minimapStartTime &&
- minimapEndTime &&
- segmentTime > minimapStartTime &&
- segmentTime < minimapEndTime
+ segmentTime >= alignedMinimapStartTime &&
+ segmentTime < alignedMinimapEndTime
);
- }, [showMinimap, minimapStartTime, minimapEndTime, segmentTime]);
+ }, [
+ showMinimap,
+ alignedMinimapStartTime,
+ alignedMinimapEndTime,
+ segmentTime,
+ ]);
const isFirstSegmentInMinimap = useMemo(() => {
return showMinimap && segmentTime === alignedMinimapStartTime;
@@ -236,11 +238,17 @@ export function EventSegment({
}
}, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]);
- const segmentClasses = `flex flex-row ${
- showMinimap ? (isInMinimapRange ? "bg-muted" : "bg-background") : ""
+ const segmentClasses = `h-2 relative w-[55px] md:w-[100px] ${
+ showMinimap
+ ? isInMinimapRange
+ ? "bg-card"
+ : isLastSegmentInMinimap
+ ? ""
+ : "opacity-70"
+ : ""
} ${
isFirstSegmentInMinimap || isLastSegmentInMinimap
- ? "relative h-2 border-b border-gray-500"
+ ? "relative h-2 border-b-2 border-gray-500"
: ""
}`;
@@ -280,7 +288,11 @@ export function EventSegment({
}, [startTimestamp]);
return (
-
+
-
+
(
{severityValue === displaySeverityType && (
-
+
-
+
-
-
-
-
+
+
+
+
+
+
-
+
)}
{severityValue !== displaySeverityType && (
-
+
,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+))
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/web/src/hooks/use-event-utils.ts b/web/src/hooks/use-event-utils.ts
index b8483d8e6..ac4300c5d 100644
--- a/web/src/hooks/use-event-utils.ts
+++ b/web/src/hooks/use-event-utils.ts
@@ -42,7 +42,7 @@ export const useEventUtils = (
[segmentDuration]
);
- const alignDateToTimeline = useCallback(
+ const alignEndDateToTimeline = useCallback(
(time: number): number => {
const remainder = time % segmentDuration;
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
@@ -51,11 +51,21 @@ export const useEventUtils = (
[segmentDuration]
);
+ const alignStartDateToTimeline = useCallback(
+ (time: number): number => {
+ const remainder = time % segmentDuration;
+ const adjustment = remainder === 0 ? 0 : -(remainder);
+ return time + adjustment;
+ },
+ [segmentDuration]
+ );
+
return {
isStartOfEvent,
isEndOfEvent,
getSegmentStart,
getSegmentEnd,
- alignDateToTimeline,
+ alignEndDateToTimeline,
+ alignStartDateToTimeline,
};
};
diff --git a/web/src/hooks/use-handle-dragging.ts b/web/src/hooks/use-handle-dragging.ts
index cf887095f..9f70374b2 100644
--- a/web/src/hooks/use-handle-dragging.ts
+++ b/web/src/hooks/use-handle-dragging.ts
@@ -4,7 +4,8 @@ interface DragHandlerProps {
contentRef: React.RefObject;
timelineRef: React.RefObject;
scrollTimeRef: React.RefObject;
- alignDateToTimeline: (time: number) => number;
+ alignStartDateToTimeline: (time: number) => number;
+ alignEndDateToTimeline: (time: number) => number;
segmentDuration: number;
showHandlebar: boolean;
timelineDuration: number;
@@ -20,7 +21,7 @@ function useDraggableHandler({
contentRef,
timelineRef,
scrollTimeRef,
- alignDateToTimeline,
+ alignStartDateToTimeline,
segmentDuration,
showHandlebar,
timelineDuration,
@@ -94,7 +95,7 @@ function useDraggableHandler({
);
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
- const segmentStartTime = alignDateToTimeline(
+ const segmentStartTime = alignStartDateToTimeline(
timelineStart - segmentIndex * segmentDuration
);
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index dbc20e84e..d6057a184 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -82,7 +82,7 @@ export default function EventView({
};
}, [reviewPages]);
- const { alignDateToTimeline } = useEventUtils(
+ const { alignStartDateToTimeline } = useEventUtils(
reviewItems.all,
segmentDuration
);
@@ -270,7 +270,8 @@ export default function EventView({
ref={lastRow ? lastReviewRef : minimapRef}
data-start={value.start_time}
data-segment-start={
- alignDateToTimeline(value.start_time) - segmentDuration
+ alignStartDateToTimeline(value.start_time) -
+ segmentDuration
}
className="outline outline-offset-1 outline-0 rounded-lg shadow-none transition-all duration-500 my-1 md:my-0"
>
@@ -291,7 +292,7 @@ export default function EventView({
)}
-
+
Date: Wed, 28 Feb 2024 07:16:16 -0700
Subject: [PATCH 2/3] Use persistence for live layout (#10114)
* Use persistence for live layout
* Fix typing
* Fix persistence typing
* remove type
* More type fixing
---
web/src/components/camera/DebugCameraImage.tsx | 6 +++---
web/src/hooks/use-camera-live-mode.ts | 8 ++++----
web/src/hooks/use-persistence.ts | 16 ++++++++--------
web/src/pages/Live.tsx | 4 +++-
web/src/types/frigateConfig.ts | 4 +++-
5 files changed, 21 insertions(+), 17 deletions(-)
diff --git a/web/src/components/camera/DebugCameraImage.tsx b/web/src/components/camera/DebugCameraImage.tsx
index e7352c17a..34588aa06 100644
--- a/web/src/components/camera/DebugCameraImage.tsx
+++ b/web/src/components/camera/DebugCameraImage.tsx
@@ -22,7 +22,7 @@ export default function DebugCameraImage({
cameraConfig,
}: DebugCameraImageProps) {
const [showSettings, setShowSettings] = useState(false);
- const [options, setOptions] = usePersistence(
+ const [options, setOptions] = usePersistence(
`${cameraConfig?.name}-feed`,
emptyObject
);
@@ -36,7 +36,7 @@ export default function DebugCameraImage({
const searchParams = useMemo(
() =>
new URLSearchParams(
- Object.keys(options).reduce((memo, key) => {
+ Object.keys(options || {}).reduce((memo, key) => {
//@ts-ignore we know this is correct
memo.push([key, options[key] === true ? "1" : "0"]);
return memo;
@@ -68,7 +68,7 @@ export default function DebugCameraImage({
diff --git a/web/src/hooks/use-camera-live-mode.ts b/web/src/hooks/use-camera-live-mode.ts
index 214713dc1..f13622556 100644
--- a/web/src/hooks/use-camera-live-mode.ts
+++ b/web/src/hooks/use-camera-live-mode.ts
@@ -7,7 +7,7 @@ import { LivePlayerMode } from "@/types/live";
export default function useCameraLiveMode(
cameraConfig: CameraConfig,
preferredMode?: string
-): LivePlayerMode {
+): LivePlayerMode | undefined {
const { data: config } = useSWR("config");
const restreamEnabled = useMemo(() => {
@@ -22,10 +22,10 @@ export default function useCameraLiveMode(
)
);
}, [config, cameraConfig]);
- const defaultLiveMode = useMemo(() => {
+ const defaultLiveMode = useMemo(() => {
if (config && cameraConfig) {
if (restreamEnabled) {
- return cameraConfig.ui.live_mode || config?.ui.live_mode;
+ return cameraConfig.ui.live_mode || config.ui.live_mode;
}
return "jsmpeg";
@@ -33,7 +33,7 @@ export default function useCameraLiveMode(
return undefined;
}, [cameraConfig, restreamEnabled]);
- const [viewSource] = usePersistence(
+ const [viewSource] = usePersistence(
`${cameraConfig.name}-source`,
defaultLiveMode
);
diff --git a/web/src/hooks/use-persistence.ts b/web/src/hooks/use-persistence.ts
index 48a03d7e1..3d61cfa17 100644
--- a/web/src/hooks/use-persistence.ts
+++ b/web/src/hooks/use-persistence.ts
@@ -1,21 +1,21 @@
import { useEffect, useState, useCallback } from "react";
import { get as getData, set as setData } from "idb-keyval";
-type usePersistenceReturn = [
- value: any | undefined,
- setValue: (value: string | boolean) => void,
+type usePersistenceReturn = [
+ value: S | undefined,
+ setValue: (value: S) => void,
loaded: boolean,
];
-export function usePersistence(
+export function usePersistence(
key: string,
- defaultValue: any | undefined = undefined
-): usePersistenceReturn {
- const [value, setInternalValue] = useState(defaultValue);
+ defaultValue: S | undefined = undefined
+): usePersistenceReturn {
+ const [value, setInternalValue] = useState(defaultValue);
const [loaded, setLoaded] = useState(false);
const setValue = useCallback(
- (value: string | boolean) => {
+ (value: S) => {
setInternalValue(value);
async function update() {
await setData(key, value);
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx
index cb6ada236..e4c9f71a2 100644
--- a/web/src/pages/Live.tsx
+++ b/web/src/pages/Live.tsx
@@ -5,6 +5,7 @@ import LivePlayer from "@/components/player/LivePlayer";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { TooltipProvider } from "@/components/ui/tooltip";
+import { usePersistence } from "@/hooks/use-persistence";
import { Event as FrigateEvent } from "@/types/event";
import { FrigateConfig } from "@/types/frigateConfig";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -17,7 +18,8 @@ function Live() {
// layout
- const [layout, setLayout] = useState<"grid" | "list">(
+ const [layout, setLayout] = usePersistence<"grid" | "list">(
+ "live-layout",
isDesktop ? "grid" : "list"
);
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts
index 77c51c879..5149264d8 100644
--- a/web/src/types/frigateConfig.ts
+++ b/web/src/types/frigateConfig.ts
@@ -1,10 +1,12 @@
+import { LivePlayerMode } from "./live";
+
export interface UiConfig {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
- live_mode?: string;
+ live_mode?: LivePlayerMode;
use_experimental?: boolean;
dashboard: boolean;
order: number;
From a978adc5a9b502941d90c80f6d51d67a07077c5a Mon Sep 17 00:00:00 2001
From: Nicolas Mowen
Date: Wed, 28 Feb 2024 07:16:32 -0700
Subject: [PATCH 3/3] Fix reload (#10109)
* Fix reloading data
* Don't show new review data when not looking at last 24 hours
* Fix refresh button and no items text
* Cleanup
---
web/src/pages/Events.tsx | 12 +++++-------
web/src/pages/Logs.tsx | 7 ++++---
web/src/views/events/EventView.tsx | 26 +++++++++++++++-----------
3 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index f3d185c16..fc821a06f 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -28,9 +28,10 @@ export default function Events() {
// review paging
+ const [beforeTs, setBeforeTs] = useState(Date.now() / 1000);
const last24Hours = useMemo(() => {
- return { before: Date.now() / 1000, after: getHoursAgo(24) };
- }, []);
+ return { before: beforeTs, after: getHoursAgo(24) };
+ }, [beforeTs]);
const selectedTimeRange = useMemo(() => {
if (reviewSearchParams["after"] == undefined) {
return last24Hours;
@@ -73,7 +74,7 @@ export default function Events() {
};
return ["review", params];
},
- [reviewSearchParams]
+ [reviewSearchParams, last24Hours]
);
const {
@@ -96,10 +97,7 @@ export default function Events() {
setSize(size + 1);
}, [size]);
- const reloadData = useCallback(() => {
- setSize(1);
- updateSegments();
- }, []);
+ const reloadData = useCallback(() => setBeforeTs(Date.now() / 1000), []);
// preview videos
diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx
index f912ed3b5..591fe30f9 100644
--- a/web/src/pages/Logs.tsx
+++ b/web/src/pages/Logs.tsx
@@ -97,8 +97,9 @@ function Logs() {
{!endVisible && (
-
contentRef.current?.scrollTo({
top: contentRef.current?.scrollHeight,
@@ -107,7 +108,7 @@ function Logs() {
}
>
Jump to Bottom
-
+
)}
-
+ {isMobile && (
+
+ )}
-
+ {filter?.before == undefined && (
+
+ )}
- {reachedEnd && currentItems == null && (
+ {!isValidating && currentItems == null && (
There are no {severity} items to review
@@ -287,9 +291,9 @@ export default function EventView({
);
})
- ) : (
+ ) : severity != "alert" ? (
- )}
+ ) : null}