diff --git a/web/src/components/card/HistoryCard.tsx b/web/src/components/card/HistoryCard.tsx
index 3cb151bf8..cc875b654 100644
--- a/web/src/components/card/HistoryCard.tsx
+++ b/web/src/components/card/HistoryCard.tsx
@@ -72,10 +72,10 @@ export default function HistoryCard({
{timeline.camera.replaceAll("_", " ")}
Activity:
- {Object.entries(timeline.entries).map(([_, entry]) => {
+ {Object.entries(timeline.entries).map(([_, entry], idx) => {
return (
{getTimelineIcon(entry)}
diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx
index 871ff0e9e..db11d2daa 100644
--- a/web/src/components/player/PreviewThumbnailPlayer.tsx
+++ b/web/src/components/player/PreviewThumbnailPlayer.tsx
@@ -92,8 +92,6 @@ export default function PreviewThumbnailPlayer({
{
threshold: 1.0,
root: document.getElementById("pageRoot"),
- // iOS has bug where poster is empty frame until video starts playing so playback needs to begin earlier
- rootMargin: isSafari ? "10% 0px 25% 0px" : "0px",
}
);
if (node) autoPlayObserver.current.observe(node);
@@ -132,7 +130,7 @@ export default function PreviewThumbnailPlayer({
{
playerRef.current = player;
+ player.pause(); // autoplay + pause is required for iOS
player.playbackRate(isSafari ? 2 : 8);
player.currentTime(startTs - relevantPreview.start);
}}
diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx
index 35fcc3e90..519647319 100644
--- a/web/src/components/scrubber/ActivityScrubber.tsx
+++ b/web/src/components/scrubber/ActivityScrubber.tsx
@@ -74,6 +74,7 @@ const domEvents: TimelineEventsWithMissing[] = [
];
type ActivityScrubberProps = {
+ className?: string;
items?: TimelineItem[];
timeBars?: { time: DateType; id?: IdType | undefined }[];
groups?: TimelineGroup[];
@@ -81,6 +82,7 @@ type ActivityScrubberProps = {
} & TimelineEventsHandlers;
function ActivityScrubber({
+ className,
items,
timeBars,
groups,
@@ -159,7 +161,7 @@ function ActivityScrubber({
return () => {
timelineInstance.destroy();
};
- }, []);
+ }, [containerRef]);
useEffect(() => {
if (!timelineRef.current.timeline) {
@@ -184,7 +186,11 @@ function ActivityScrubber({
if (items) timelineRef.current.timeline.setItems(items);
}, [items, groups, options, currentTime, eventHandlers]);
- return ;
+ return (
+
+ );
}
export default ActivityScrubber;
diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx
index de31d9031..e51f78f89 100644
--- a/web/src/components/ui/button.tsx
+++ b/web/src/components/ui/button.tsx
@@ -21,6 +21,7 @@ const buttonVariants = cva(
},
size: {
default: "h-10 px-4 py-2",
+ xs: "h-6 rounded-md",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx
new file mode 100644
index 000000000..ee4ccaeca
--- /dev/null
+++ b/web/src/hooks/use-overlay-state.tsx
@@ -0,0 +1,20 @@
+import { useCallback } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+export default function useOverlayState(key: string) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const currentLocationState = location.state;
+
+ const setOverlayStateValue = useCallback(
+ (value: string) => {
+ const newLocationState = { ...currentLocationState };
+ newLocationState[key] = value;
+ navigate(location.pathname, { state: newLocationState });
+ },
+ [navigate]
+ );
+
+ const overlayStateValue = location.state && location.state[key];
+ return [overlayStateValue, setOverlayStateValue];
+}
diff --git a/web/src/pages/History.tsx b/web/src/pages/History.tsx
index 502a45550..cc0bca56a 100644
--- a/web/src/pages/History.tsx
+++ b/web/src/pages/History.tsx
@@ -22,6 +22,9 @@ import HistoryCardView from "@/views/history/HistoryCardView";
import HistoryTimelineView from "@/views/history/HistoryTimelineView";
import { Button } from "@/components/ui/button";
import { IoMdArrowBack } from "react-icons/io";
+import useOverlayState from "@/hooks/use-overlay-state";
+import { useNavigate } from "react-router-dom";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
const API_LIMIT = 200;
@@ -81,10 +84,24 @@ function History() {
{ revalidateOnFocus: false }
);
+ const navigate = useNavigate();
const [playback, setPlayback] = useState();
+ const [viewingPlayback, setViewingPlayback] = useOverlayState("timeline");
+ const setPlaybackState = useCallback(
+ (playback: TimelinePlayback | undefined) => {
+ if (playback == undefined) {
+ setPlayback(undefined);
+ navigate(-1);
+ } else {
+ setPlayback(playback);
+ setViewingPlayback(true);
+ }
+ },
+ [navigate]
+ );
- const shouldAutoPlay = useMemo(() => {
- return playback == undefined && window.innerWidth < 480;
+ const isMobile = useMemo(() => {
+ return window.innerWidth < 768;
}, [playback]);
const timelineCards: CardsData | never[] = useMemo(() => {
@@ -142,12 +159,13 @@ function History() {
return (
<>
-
- {playback != undefined && (
+
+ {viewingPlayback && (
@@ -186,27 +204,51 @@ function History() {
- <>
- {playback == undefined && (
-
{
- setSize(size + 1);
- }}
- onDelete={onDelete}
- onItemSelected={(item) => setPlayback(item)}
- />
- )}
- {playback != undefined && (
-
- )}
- >
+ {
+ setSize(size + 1);
+ }}
+ onDelete={onDelete}
+ onItemSelected={(item) => setPlaybackState(item)}
+ />
+ setPlaybackState(undefined)}
+ />
>
);
}
+type TimelineViewerProps = {
+ playback: TimelinePlayback | undefined;
+ isMobile: boolean;
+ onClose: () => void;
+};
+
+function TimelineViewer({ playback, isMobile, onClose }: TimelineViewerProps) {
+ if (isMobile) {
+ return playback != undefined ? (
+
+
+
+ ) : null;
+ }
+
+ return (
+
+ );
+}
+
export default History;
diff --git a/web/src/views/history/HistoryTimelineView.tsx b/web/src/views/history/HistoryTimelineView.tsx
index bd96792e5..98a4baca1 100644
--- a/web/src/views/history/HistoryTimelineView.tsx
+++ b/web/src/views/history/HistoryTimelineView.tsx
@@ -11,7 +11,7 @@ import {
getTimelineIcon,
} from "@/utils/timelineUtil";
import { renderToStaticMarkup } from "react-dom/server";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import useSWR from "swr";
import Player from "video.js/dist/types/player";
@@ -41,6 +41,8 @@ export default function HistoryTimelineView({
const [focusedItem, setFocusedItem] = useState(
undefined
);
+
+ const [seeking, setSeeking] = useState(false);
const [timeToSeek, setTimeToSeek] = useState(undefined);
const annotationOffset = useMemo(() => {
@@ -66,7 +68,10 @@ export default function HistoryTimelineView({
const startTime = date.getTime() / 1000;
date.setHours(date.getHours() + 1);
const endTime = date.getTime() / 1000;
- return { start: startTime.toFixed(1), end: endTime.toFixed(1) };
+ return {
+ start: parseInt(startTime.toFixed(1)),
+ end: parseInt(endTime.toFixed(1)),
+ };
}, [timelineTime]);
const recordingParams = useMemo(() => {
@@ -85,7 +90,7 @@ export default function HistoryTimelineView({
return "";
}
- const date = new Date(parseInt(playbackTimes.start) * 1000);
+ const date = new Date(playbackTimes.start * 1000);
return `${apiHost}vod/${date.getFullYear()}-${
date.getMonth() + 1
}/${date.getDate()}/${date.getHours()}/${
@@ -129,41 +134,53 @@ export default function HistoryTimelineView({
[annotationOffset, recordings, playerRef]
);
- const onScrubTime = (data: { time: Date }) => {
- if (!hasRelevantPreview) {
+ const onScrubTime = useCallback(
+ (data: { time: Date }) => {
+ if (!hasRelevantPreview) {
+ return;
+ }
+
+ if (playerRef.current?.paused() == false) {
+ setScrubbing(true);
+ playerRef.current?.pause();
+ }
+
+ const seekTimestamp = data.time.getTime() / 1000;
+ const seekTime = seekTimestamp - playback.relevantPreview!!.start;
+ setTimeToSeek(Math.round(seekTime));
+ },
+ [scrubbing, playerRef]
+ );
+
+ const onStopScrubbing = useCallback(
+ (data: { time: Date }) => {
+ const playbackTime = data.time.getTime() / 1000;
+ playerRef.current?.currentTime(playbackTime - playbackTimes.start);
+ setScrubbing(false);
+ playerRef.current?.play();
+ },
+ [playerRef]
+ );
+
+ // handle seeking to next frame when seek is finished
+ useEffect(() => {
+ if (seeking) {
return;
}
- if (!scrubbing) {
- playerRef.current?.pause();
- setScrubbing(true);
+ if (timeToSeek && timeToSeek != previewRef.current?.currentTime()) {
+ setSeeking(true);
+ previewRef.current?.currentTime(timeToSeek);
}
-
- 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);
- };
+ }, [timeToSeek, seeking]);
if (!config || !recordings) {
return ;
}
return (
- <>
-
+
+ <>
{
playerRef.current = player;
- player.currentTime(timelineTime - parseInt(playbackTimes.start));
+ player.currentTime(timelineTime - playbackTimes.start);
player.on("playing", () => {
setFocusedItem(undefined);
});
@@ -219,7 +236,7 @@ export default function HistoryTimelineView({
seekOptions={{}}
onReady={(player) => {
previewRef.current = player;
- player.on("seeked", onSeeked);
+ player.on("seeked", () => setSeeking(false));
}}
onDispose={() => {
previewRef.current = undefined;
@@ -227,36 +244,37 @@ export default function HistoryTimelineView({
/>
)}
-
+ >
{playback != undefined && (
{
- const playbackTime = data.time.getTime() / 1000;
- playerRef.current?.currentTime(
- playbackTime - parseInt(playbackTimes.start)
- );
- setScrubbing(false);
- playerRef.current?.play();
- }}
+ timechangedHandler={onStopScrubbing}
selectHandler={onSelectItem}
/>
)}
- >
+
);
}