-
+
{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) {