From 68506373435c96cd1ff65bf0395efde5d54129f9 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Thu, 21 Dec 2023 16:11:30 -0700 Subject: [PATCH] Implement seeking limiter --- web/package-lock.json | 13 ++ web/package.json | 1 + .../components/scrubber/ActivityScrubber.tsx | 2 +- web/src/components/ui/drawer.tsx | 116 +++++++++++++ web/src/pages/History.tsx | 7 - web/src/utils/timelineUtil.tsx | 22 +++ web/src/views/history/HistoryCardView.tsx | 6 +- web/src/views/history/HistoryTimelineView.tsx | 164 +++++++++++------- web/tailwind.config.js | 3 +- 9 files changed, 261 insertions(+), 73 deletions(-) create mode 100644 web/src/components/ui/drawer.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 7df832b3a..af1204746 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -46,6 +46,7 @@ "swr": "^2.2.4", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^0.8.0", "video.js": "^8.6.1", "videojs-playlist": "^5.1.0", "vis-timeline": "^7.7.3", @@ -7758,6 +7759,18 @@ "node": ">=10.12.0" } }, + "node_modules/vaul": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.8.0.tgz", + "integrity": "sha512-9nUU2jIObJvJZxeQU1oVr/syKo5XqbRoOMoTEt0hHlWify4QZFlqTh6QSN/yxoKzNrMeEQzxbc3XC/vkPLOIqw==", + "dependencies": { + "@radix-ui/react-dialog": "^1.0.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/video.js": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.6.1.tgz", diff --git a/web/package.json b/web/package.json index b1512b431..ed225d214 100644 --- a/web/package.json +++ b/web/package.json @@ -51,6 +51,7 @@ "swr": "^2.2.4", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^0.8.0", "video.js": "^8.6.1", "videojs-playlist": "^5.1.0", "vis-timeline": "^7.7.3", diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx index a144f8e8b..35fcc3e90 100644 --- a/web/src/components/scrubber/ActivityScrubber.tsx +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -74,7 +74,7 @@ const domEvents: TimelineEventsWithMissing[] = [ ]; type ActivityScrubberProps = { - items: TimelineItem[]; + items?: TimelineItem[]; timeBars?: { time: DateType; id?: IdType | undefined }[]; groups?: TimelineGroup[]; options?: TimelineOptions; diff --git a/web/src/components/ui/drawer.tsx b/web/src/components/ui/drawer.tsx new file mode 100644 index 000000000..c17b0ccaa --- /dev/null +++ b/web/src/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/web/src/pages/History.tsx b/web/src/pages/History.tsx index 8f76a2744..502a45550 100644 --- a/web/src/pages/History.tsx +++ b/web/src/pages/History.tsx @@ -5,7 +5,6 @@ import { FrigateConfig } from "@/types/frigateConfig"; import Heading from "@/components/ui/heading"; import ActivityIndicator from "@/components/ui/activity-indicator"; import axios from "axios"; -import TimelinePlayerCard from "@/components/card/TimelinePlayerCard"; import { getHourlyTimelineData } from "@/utils/historyUtil"; import { AlertDialog, @@ -187,12 +186,6 @@ function History() { - - setPlayback(undefined)} - /> - <> {playback == undefined && ( ; + case "dog": + return ; + case "package": + return ; + default: + return ; + } +} + export function getTimelineItemDescription(timelineItem: Timeline) { const label = ( (Array.isArray(timelineItem.data.sub_label) diff --git a/web/src/views/history/HistoryCardView.tsx b/web/src/views/history/HistoryCardView.tsx index bbaba938c..b500e3598 100644 --- a/web/src/views/history/HistoryCardView.tsx +++ b/web/src/views/history/HistoryCardView.tsx @@ -9,7 +9,7 @@ import useSWR from "swr"; type HistoryCardViewProps = { timelineCards: CardsData | never[]; allPreviews: Preview[] | undefined; - isMobileView: boolean; + isMobile: boolean; isValidating: boolean; isDone: boolean; onNextPage: () => void; @@ -20,7 +20,7 @@ type HistoryCardViewProps = { export default function HistoryCardView({ timelineCards, allPreviews, - isMobileView, + isMobile, isValidating, isDone, onNextPage, @@ -112,7 +112,7 @@ export default function HistoryCardView({ { onItemSelected({ diff --git a/web/src/views/history/HistoryTimelineView.tsx b/web/src/views/history/HistoryTimelineView.tsx index 674a449e8..bd96792e5 100644 --- a/web/src/views/history/HistoryTimelineView.tsx +++ b/web/src/views/history/HistoryTimelineView.tsx @@ -1,10 +1,16 @@ import { useApiHost } from "@/api"; import TimelineEventOverlay from "@/components/overlay/TimelineDataOverlay"; import VideoPlayer from "@/components/player/VideoPlayer"; -import ActivityScrubber from "@/components/scrubber/ActivityScrubber"; +import ActivityScrubber, { + ScrubberItem, +} from "@/components/scrubber/ActivityScrubber"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { FrigateConfig } from "@/types/frigateConfig"; -import { getTimelineItemDescription } from "@/utils/timelineUtil"; +import { + getTimelineDetectionIcon, + getTimelineIcon, +} from "@/utils/timelineUtil"; +import { renderToStaticMarkup } from "react-dom/server"; import { useCallback, useMemo, useRef, useState } from "react"; import useSWR from "swr"; import Player from "video.js/dist/types/player"; @@ -26,6 +32,8 @@ export default function HistoryTimelineView({ [config] ); + const hasRelevantPreview = playback.relevantPreview != undefined; + const playerRef = useRef(undefined); const previewRef = useRef(undefined); @@ -33,6 +41,7 @@ export default function HistoryTimelineView({ const [focusedItem, setFocusedItem] = useState( undefined ); + const [timeToSeek, setTimeToSeek] = useState(undefined); const annotationOffset = useMemo(() => { if (!config) { @@ -120,52 +129,78 @@ export default function HistoryTimelineView({ [annotationOffset, recordings, playerRef] ); + const onScrubTime = (data: { time: Date }) => { + if (!hasRelevantPreview) { + return; + } + + if (!scrubbing) { + playerRef.current?.pause(); + setScrubbing(true); + } + + const seekTimestamp = data.time.getTime() / 1000; + const seekTime = seekTimestamp - playback.relevantPreview!!.start; + if (timeToSeek != undefined) { + setTimeToSeek(seekTime); + } else { + setTimeToSeek(seekTime); + previewRef.current?.currentTime(seekTime); + } + }; + + const onSeeked = () => { + if (timeToSeek && playerRef.current?.currentTime() != timeToSeek) { + playerRef.current?.currentTime(timeToSeek); + } + + setTimeToSeek(undefined); + }; + if (!config || !recordings) { return ; } return ( <> -
-
-
- { - playerRef.current = player; - player.currentTime( - timelineTime - parseInt(playbackTimes.start) - ); - player.on("playing", () => { - setFocusedItem(undefined); - }); - }} - onDispose={() => { - playerRef.current = undefined; - }} - > - {config && focusedItem ? ( - - ) : undefined} - -
+
+
+ { + playerRef.current = player; + player.currentTime(timelineTime - parseInt(playbackTimes.start)); + player.on("playing", () => { + setFocusedItem(undefined); + }); + }} + onDispose={() => { + playerRef.current = undefined; + }} + > + {config && focusedItem ? ( + + ) : undefined} + +
+ {hasRelevantPreview && (
{ previewRef.current = player; + player.on("seeked", onSeeked); }} onDispose={() => { previewRef.current = undefined; }} />
+ )} +
+
+ {playback != undefined && ( { - if (!scrubbing) { - playerRef.current?.pause(); - setScrubbing(true); - } - - const seekTimestamp = data.time.getTime() / 1000; - previewRef.current?.currentTime( - seekTimestamp - playback.relevantPreview!!.start - ); + timeAxis: isMobile ? { scale: "minute" } : {}, }} + timechangeHandler={onScrubTime} timechangedHandler={(data) => { const playbackTime = data.time.getTime() / 1000; playerRef.current?.currentTime( @@ -220,22 +254,30 @@ export default function HistoryTimelineView({ }} selectHandler={onSelectItem} /> -
+ )}
); } -function timelineItemsToScrubber(items: Timeline[]) { +function timelineItemsToScrubber(items: Timeline[]): ScrubberItem[] { return items.map((item) => { return { id: item.timestamp, - content: `
${getTimelineItemDescription( - item - )}
`, + content: getTimelineContentElement(item), start: new Date(item.timestamp * 1000), end: new Date(item.timestamp * 1000), type: "box", }; }); } + +function getTimelineContentElement(item: Timeline): HTMLElement { + const output = document.createElement(`div-${item.timestamp}`); + output.innerHTML = renderToStaticMarkup( +
+ {getTimelineDetectionIcon(item)} : {getTimelineIcon(item)} +
+ ); + return output; +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index e6180eacd..c52bb7959 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -72,7 +72,8 @@ module.exports = { }, screens: { "xs": "480px", - "2xl": "1400px", + "2xl": "1440px", + "3xl": "1920px", }, }, },