diff --git a/web/src/components/card/HistoryCard.tsx b/web/src/components/card/HistoryCard.tsx index cc875b654..4c2b9ca9b 100644 --- a/web/src/components/card/HistoryCard.tsx +++ b/web/src/components/card/HistoryCard.tsx @@ -10,11 +10,12 @@ import { getTimelineIcon, getTimelineItemDescription, } from "@/utils/timelineUtil"; +import { Button } from "../ui/button"; type HistoryCardProps = { timeline: Card; relevantPreview?: Preview; - shouldAutoPlay: boolean; + isMobile: boolean; onClick?: () => void; onDelete?: () => void; }; @@ -22,7 +23,7 @@ type HistoryCardProps = { export default function HistoryCard({ relevantPreview, timeline, - shouldAutoPlay, + isMobile, onClick, onDelete, }: HistoryCardProps) { @@ -42,11 +43,12 @@ export default function HistoryCard({ relevantPreview={relevantPreview} startTs={Object.values(timeline.entries)[0].timestamp} eventId={Object.values(timeline.entries)[0].source_id} - shouldAutoPlay={shouldAutoPlay} + isMobile={isMobile} + onClick={onClick} /> -
+ <>
-
+
{formatUnixTimestampToDateTime(timeline.time, { strftime_fmt: @@ -55,35 +57,38 @@ export default function HistoryCard({ date_style: "medium", })}
- { - e.stopPropagation(); +
-
+
{timeline.camera.replaceAll("_", " ")}
-
Activity:
- {Object.entries(timeline.entries).map(([_, entry], idx) => { - return ( -
- {getTimelineIcon(entry)} - {getTimelineItemDescription(entry)} -
- ); - })} -
+
+
Activity:
+ {Object.entries(timeline.entries).map(([_, entry], idx) => { + return ( +
+ {getTimelineIcon(entry)} + {getTimelineItemDescription(entry)} +
+ ); + })} +
+ ); } diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index db11d2daa..521d9c0e2 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -1,7 +1,13 @@ import { FrigateConfig } from "@/types/frigateConfig"; import VideoPlayer from "./VideoPlayer"; import useSWR from "swr"; -import { useCallback, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useApiHost } from "@/api"; import Player from "video.js/dist/types/player"; import { AspectRatio } from "../ui/aspect-ratio"; @@ -12,7 +18,8 @@ type PreviewPlayerProps = { relevantPreview?: Preview; startTs: number; eventId: string; - shouldAutoPlay: boolean; + isMobile: boolean; + onClick?: () => void; }; type Preview = { @@ -28,11 +35,11 @@ export default function PreviewThumbnailPlayer({ relevantPreview, startTs, eventId, - shouldAutoPlay, + isMobile, + onClick, }: PreviewPlayerProps) { const { data: config } = useSWR("config"); const playerRef = useRef(null); - const apiHost = useApiHost(); const isSafari = useMemo(() => { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); }, []); @@ -78,7 +85,7 @@ export default function PreviewThumbnailPlayer({ } } - if (shouldAutoPlay && !autoPlayObserver.current) { + if (isMobile && !autoPlayObserver.current) { try { autoPlayObserver.current = new IntersectionObserver( (entries) => { @@ -103,20 +110,92 @@ export default function PreviewThumbnailPlayer({ [preloadObserver, autoPlayObserver, onPlayback] ); - let content; + return ( + onPlayback(true)} + onMouseLeave={() => onPlayback(false)} + > + + + ); +} + +type PreviewContentProps = { + playerRef: React.MutableRefObject; + config: FrigateConfig; + camera: string; + relevantPreview: Preview | undefined; + eventId: string; + visible: boolean; + startTs: number; + isMobile: boolean; + isSafari: boolean; + onClick?: () => void; +}; +function PreviewContent({ + playerRef, + config, + camera, + relevantPreview, + eventId, + visible, + startTs, + isMobile, + isSafari, + onClick, +}: PreviewContentProps) { + const apiHost = useApiHost(); + + // handle touchstart -> touchend as click + const [touchStart, setTouchStart] = useState(0); + const handleTouchStart = useCallback(() => { + setTouchStart(new Date().getTime()); + }, []); + useEffect(() => { + if (!isMobile || !playerRef.current || !onClick) { + return; + } + + playerRef.current.on("touchend", () => { + if (!onClick) { + return; + } + + const touchEnd = new Date().getTime(); + + // consider tap less than 500 ms + if (touchEnd - touchStart < 500) { + onClick(); + } + }); + }, [playerRef, touchStart]); if (relevantPreview && !visible) { - content =
; + return
; } else if (!relevantPreview) { if (isCurrentHour(startTs)) { - content = ( + return ( ); } else { - content = ( + return (
{ playerRef.current = null; @@ -157,18 +239,6 @@ export default function PreviewThumbnailPlayer({ ); } - - return ( - onPlayback(true)} - onMouseLeave={() => onPlayback(false)} - > - {content} - - ); } function isCurrentHour(timestamp: number) { diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx index 519647319..9f43e98c5 100644 --- a/web/src/components/scrubber/ActivityScrubber.tsx +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -129,11 +129,16 @@ function ActivityScrubber({ return; } + const timelineOptions: TimelineOptions = { + ...defaultOptions, + ...options, + }; + const timelineInstance = new VisTimeline( divElement, items as DataItem[], groups as DataGroup[], - options + timelineOptions ); if (timeBars) { @@ -151,41 +156,11 @@ function ActivityScrubber({ timelineRef.current.timeline = timelineInstance; - const timelineOptions: TimelineOptions = { - ...defaultOptions, - ...options, - }; - - timelineInstance.setOptions(timelineOptions); - return () => { timelineInstance.destroy(); }; }, [containerRef]); - useEffect(() => { - if (!timelineRef.current.timeline) { - return; - } - - // If the currentTime updates, adjust the scrubber's end date and max - // May not be applicable to all scrubbers, might want to just pass this in - // for any scrubbers that we want to dynamically move based on time - // const updatedTimeOptions: TimelineOptions = { - // end: currentTime, - // max: currentTime, - // }; - - const timelineOptions: TimelineOptions = { - ...defaultOptions, - // ...updatedTimeOptions, - ...options, - }; - - timelineRef.current.timeline.setOptions(timelineOptions); - if (items) timelineRef.current.timeline.setItems(items); - }, [items, groups, options, currentTime, eventHandlers]); - return (
diff --git a/web/src/utils/timelineUtil.tsx b/web/src/utils/timelineUtil.tsx index 2c17e0768..1820df5f5 100644 --- a/web/src/utils/timelineUtil.tsx +++ b/web/src/utils/timelineUtil.tsx @@ -1,21 +1,25 @@ import { + LuCamera, + LuCar, + LuCat, LuCircle, LuCircleDot, LuDog, LuEar, - LuFocus, LuPackage, LuPersonStanding, LuPlay, LuPlayCircle, LuTruck, } from "react-icons/lu"; +import { GiDeer } from "react-icons/gi"; import { IoMdExit } from "react-icons/io"; import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt, } from "react-icons/md"; +import { FaBicycle } from "react-icons/fa"; export function getTimelineIcon(timelineItem: Timeline) { switch (timelineItem.class_type) { @@ -61,14 +65,22 @@ export function getTimelineIcon(timelineItem: Timeline) { */ export function getTimelineDetectionIcon(timelineItem: Timeline) { switch (timelineItem.data.label) { - case "person": - return ; + case "bicycle": + return ; + case "car": + return ; + case "cat": + return ; + case "deer": + return ; case "dog": return ; case "package": return ; + case "person": + return ; default: - return ; + return ; } } diff --git a/web/src/views/history/HistoryCardView.tsx b/web/src/views/history/HistoryCardView.tsx index b500e3598..eb28ca238 100644 --- a/web/src/views/history/HistoryCardView.tsx +++ b/web/src/views/history/HistoryCardView.tsx @@ -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 98a4baca1..a892f8e9d 100644 --- a/web/src/views/history/HistoryTimelineView.tsx +++ b/web/src/views/history/HistoryTimelineView.tsx @@ -110,7 +110,6 @@ export default function HistoryTimelineView({ playerRef.current?.pause(); let seekSeconds = 0; - console.log("recordings are " + recordings?.length); (recordings || []).every((segment) => { // if the next segment is past the desired time, stop calculating if (segment.start_time > selected) {