From 920c41d4c9dcc3c86c3aea6ddecd497e6f989a9a Mon Sep 17 00:00:00 2001 From: JohnMark Sill Date: Wed, 16 Feb 2022 10:24:47 -0600 Subject: [PATCH] refactor: pulled components apart and improved logic --- web/src/components/BubbleButton.tsx | 47 +++ web/src/components/HistoryViewer.jsx | 174 ----------- .../HistoryViewer/HistoryHeader.tsx | 30 ++ .../components/HistoryViewer/HistoryVideo.tsx | 137 +++++++++ .../HistoryViewer/HistoryViewer.tsx | 81 +++++ web/src/components/Timeline/Timeline.tsx | 288 +++++++++++------- .../components/Timeline/TimelineBlocks.tsx | 36 ++- .../components/Timeline/TimelineControls.tsx | 34 +++ web/src/routes/Camera_V2.jsx | 4 +- web/src/routes/HistoryHeader.jsx | 15 - 10 files changed, 547 insertions(+), 299 deletions(-) create mode 100644 web/src/components/BubbleButton.tsx delete mode 100644 web/src/components/HistoryViewer.jsx create mode 100644 web/src/components/HistoryViewer/HistoryHeader.tsx create mode 100644 web/src/components/HistoryViewer/HistoryVideo.tsx create mode 100644 web/src/components/HistoryViewer/HistoryViewer.tsx create mode 100644 web/src/components/Timeline/TimelineControls.tsx delete mode 100644 web/src/routes/HistoryHeader.jsx diff --git a/web/src/components/BubbleButton.tsx b/web/src/components/BubbleButton.tsx new file mode 100644 index 000000000..f0293bb08 --- /dev/null +++ b/web/src/components/BubbleButton.tsx @@ -0,0 +1,47 @@ +import { h } from 'preact'; + +interface BubbleButtonProps { + variant?: 'primary' | 'secondary'; + children?: JSX.Element; + disabled?: boolean; + className?: string; + onClick?: () => void; +} + +export const BubbleButton = ({ + variant = 'primary', + children, + onClick, + disabled = false, + className = '', +}: BubbleButtonProps) => { + const BASE_CLASS = 'rounded-full px-4 py-2'; + const PRIMARY_CLASS = 'text-white bg-blue-500 dark:text-black dark:bg-white'; + const SECONDARY_CLASS = 'text-black dark:text-white bg-transparent'; + let computedClass = BASE_CLASS; + + if (disabled) { + computedClass += ' text-gray-200 dark:text-gray-200'; + } else { + if (variant === 'primary') { + computedClass += ` ${PRIMARY_CLASS}`; + } else if (variant === 'secondary') { + computedClass += ` ${SECONDARY_CLASS}`; + } + } + + const onClickHandler = () => { + if (disabled) { + return; + } + + if (onClick) { + onClick(); + } + }; + return ( + + ); +}; diff --git a/web/src/components/HistoryViewer.jsx b/web/src/components/HistoryViewer.jsx deleted file mode 100644 index 2951e0d2c..000000000 --- a/web/src/components/HistoryViewer.jsx +++ /dev/null @@ -1,174 +0,0 @@ -import { Fragment, h } from 'preact'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; -import { useApiHost, useEvents } from '../api'; -import { useSearchString } from '../hooks/useSearchString'; -import { Next } from '../icons/Next'; -import { Play } from '../icons/Play'; -import { Previous } from '../icons/Previous'; -import { HistoryHeader } from '../routes/HistoryHeader'; -import { longToDate } from '../utils/dateUtil'; -import Timeline from './Timeline/Timeline'; - -const getLast24Hours = () => { - return new Number(new Date(new Date().getTime() - 24 * 60 * 60 * 1000)) / 1000; -}; - -export default function HistoryViewer({ camera }) { - const apiHost = useApiHost(); - const videoRef = useRef(); - const { searchString } = useSearchString(500, `camera=${camera}&after=${getLast24Hours()}`); - const { data: events } = useEvents(searchString); - const [timelineEvents, setTimelineEvents] = useState(); - - const [hasPlayed, setHasPlayed] = useState(); - const [isPlaying, setIsPlaying] = useState(false); - const [currentEvent, setCurrentEvent] = useState(); - const [currentEventIndex, setCurrentEventIndex] = useState(); - const [timelineOffset, setTimelineOffset] = useState(0); - const [minHeight, setMinHeight] = useState(); - - useEffect(() => { - if (events) { - const filteredEvents = [...events].reverse().filter((e) => e.end_time !== undefined); - setTimelineEvents(filteredEvents); - setCurrentEventIndex(filteredEvents.length - 1); - } - }, [events]); - - const handleTimeUpdate = () => { - const videoContainer = videoRef.current; - if (videoContainer.paused) { - return; - } - - const timestamp = Math.round(videoRef.current.currentTime); - const offset = Math.round(timestamp); - const triggerStateChange = offset !== timelineOffset; - if (triggerStateChange) { - setTimelineOffset(offset); - } - }; - - const handleTimelineChange = (timelineChangedEvent) => { - if (timelineChangedEvent.seekComplete) { - setCurrentEvent(timelineChangedEvent.event); - } - - const videoContainer = videoRef.current; - if (videoContainer) { - if (!videoContainer.paused) { - videoContainer.pause(); - setHasPlayed(true); - } - - const videoHasPermissionToPlay = hasPlayed !== undefined; - if (videoHasPermissionToPlay && timelineChangedEvent.seekComplete) { - const markerTime = Math.abs(timelineChangedEvent.time - timelineChangedEvent.event.startTime) / 1000; - videoContainer.currentTime = markerTime; - if (hasPlayed) { - videoContainer.play(); - setHasPlayed(false); - } - } - } - }; - - const handlePlay = function () { - const videoContainer = videoRef.current; - if (videoContainer) { - if (videoContainer.paused) { - videoContainer.play(); - } else { - videoContainer.pause(); - } - } - }; - - const handlePlayed = () => { - setIsPlaying(true); - }; - - const handlePaused = () => { - setIsPlaying(false); - }; - - const handlePrevious = function () { - setCurrentEventIndex(currentEvent.index - 1); - }; - - const handleNext = function () { - setCurrentEventIndex(currentEvent.index + 1); - }; - - const handleMetadataLoad = () => { - const videoContainer = videoRef.current; - if (videoContainer) { - setMinHeight(videoContainer.clientHeight); - } - }; - - const RenderVideo = useCallback(() => { - if (currentEvent) { - return ( - - ); - } - }, [currentEvent, apiHost, camera, videoRef]); - - return ( - - {currentEvent && ( - -
- - -
-
- )} - - - -
- - - -
-
- ); -} diff --git a/web/src/components/HistoryViewer/HistoryHeader.tsx b/web/src/components/HistoryViewer/HistoryHeader.tsx new file mode 100644 index 000000000..5178eaada --- /dev/null +++ b/web/src/components/HistoryViewer/HistoryHeader.tsx @@ -0,0 +1,30 @@ +import { h } from 'preact'; +import Heading from '../Heading'; +import { TimelineEvent } from '../Timeline/Timeline'; + +interface HistoryHeaderProps { + event: TimelineEvent; + className?: string; +} +export const HistoryHeader = ({ event, className = '' }: HistoryHeaderProps) => { + let title = 'No Event Found'; + let subtitle = Event was not found at marker position.; + if (event) { + const { startTime, endTime, label } = event; + const thisMorning = new Date(); + thisMorning.setHours(0, 0, 0); + const isToday = endTime.getTime() > thisMorning.getTime(); + title = label; + subtitle = ( + + {isToday ? 'Today' : 'Yesterday'}, {startTime.toLocaleTimeString()} - {endTime.toLocaleTimeString()} · + + ); + } + return ( +
+ {title} +
{subtitle}
+
+ ); +}; diff --git a/web/src/components/HistoryViewer/HistoryVideo.tsx b/web/src/components/HistoryViewer/HistoryVideo.tsx new file mode 100644 index 000000000..f5dcb6fbd --- /dev/null +++ b/web/src/components/HistoryViewer/HistoryVideo.tsx @@ -0,0 +1,137 @@ +import { h } from 'preact'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useApiHost } from '../../api'; + +interface OnTimeUpdateEvent { + timestamp: number; + isPlaying: boolean; +} + +interface VideoProperties { + posterUrl: string; + videoUrl: string; + height: number; +} + +interface HistoryVideoProps { + id: string; + isPlaying: boolean; + currentTime: number; + onTimeUpdate: (event: OnTimeUpdateEvent) => void; + onPause: () => void; + onPlay: () => void; +} + +const isNullOrUndefined = (object: any): boolean => object === null || object === undefined; + +export const HistoryVideo = ({ + id, + isPlaying: videoIsPlaying, + currentTime, + onTimeUpdate, + onPause, + onPlay, +}: HistoryVideoProps) => { + const apiHost = useApiHost(); + const videoRef = useRef(); + const [videoHeight, setVideoHeight] = useState(undefined); + const [videoProperties, setVideoProperties] = useState(undefined); + + const initializeVideoContainerHeight = useCallback(() => { + const video = videoRef.current; + const videoExists = !isNullOrUndefined(video); + if (videoExists) { + const videoHeight = video.offsetHeight; + const videoHasHeight = videoHeight > 0; + if (videoHasHeight) { + setVideoHeight(videoHeight); + } + } + }, [videoRef.current]); + + useEffect(() => { + initializeVideoContainerHeight(); + }, [initializeVideoContainerHeight]); + + useEffect(() => { + const idExists = !isNullOrUndefined(id); + if (idExists) { + setVideoProperties({ + posterUrl: `${apiHost}/api/events/${id}/snapshot.jpg`, + videoUrl: `${apiHost}/vod/event/${id}/index.m3u8`, + height: videoHeight, + }); + } else { + setVideoProperties(undefined); + } + }, [id, videoHeight]); + + const playVideo = (video: HTMLMediaElement) => { + const videoHasNotLoaded = video.readyState <= 1; + if (videoHasNotLoaded) { + video.load(); + } + + video.play().catch((e) => { + console.error('Fail', e); + }); + }; + + useEffect(() => { + const video = videoRef.current; + const videoExists = !isNullOrUndefined(video); + if (videoExists) { + if (videoIsPlaying) { + playVideo(video); + } else { + video.pause(); + } + } + }, [videoIsPlaying, videoRef]); + + useEffect(() => { + const video = videoRef.current; + const videoExists = !isNullOrUndefined(video); + const hasSeeked = currentTime >= 0; + if (videoExists && hasSeeked) { + video.currentTime = currentTime; + } + }, [currentTime, videoRef]); + + const onTimeUpdateHandler = useCallback( + (event: Event) => { + const target = event.target as HTMLMediaElement; + const timeUpdateEvent = { + isPlaying: videoIsPlaying, + timestamp: target.currentTime, + }; + onTimeUpdate(timeUpdateEvent); + }, + [videoIsPlaying] + ); + + const Video = useCallback(() => { + const videoPropertiesIsUndefined = isNullOrUndefined(videoProperties); + if (videoPropertiesIsUndefined) { + return
; + } + const { posterUrl, videoUrl, height } = videoProperties; + return ( + + ); + }, [videoProperties, videoHeight, videoRef]); + + return