Improve desktop timeline view (#9150)

* Break apart mobile and desktop timeline views

* Set aspect ratio for player correctly

* more modest default width

* Add timeline item card

* Get video player to fit

* get layout going

* More work on youtube view

* Get video scaling working

* Better dialog sizes

* Show all timelines for day

* Add full day of timelines

* Improve hooks

* Fix previews

* Separate mobile and desktop views and don't rerender

* cleanup

* Optimizations and improvements

* make preview dates more efficient

* Remove seekbar and use timeline as seekbar

* Improve background and scrubbing
This commit is contained in:
Nicolas Mowen
2024-01-01 09:37:07 -06:00
committed by Blake Blackshear
parent 0ee81c7526
commit 160e331035
10 changed files with 663 additions and 88 deletions

View File

@@ -0,0 +1,76 @@
import { getTimelineItemDescription } from "@/utils/timelineUtil";
import { Button } from "../ui/button";
import Logo from "../Logo";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import VideoPlayer from "../player/VideoPlayer";
import { Card } from "../ui/card";
type TimelineItemCardProps = {
timeline: Timeline;
relevantPreview: Preview | undefined;
onSelect: () => void;
};
export default function TimelineItemCard({
timeline,
relevantPreview,
onSelect,
}: TimelineItemCardProps) {
const { data: config } = useSWR<FrigateConfig>("config");
return (
<Card className="relative m-2 flex w-full h-32 cursor-pointer" onClick={onSelect}>
<div className="w-1/2 p-2">
{relevantPreview && (
<VideoPlayer
options={{
preload: "auto",
height: "114",
width: "202",
autoplay: true,
controls: false,
fluid: false,
muted: true,
loadingSpinner: false,
sources: [
{
src: `${relevantPreview.src}`,
type: "video/mp4",
},
],
}}
seekOptions={{}}
onReady={(player) => {
player.pause(); // autoplay + pause is required for iOS
player.currentTime(timeline.timestamp - relevantPreview.start);
}}
/>
)}
</div>
<div className="px-2 py-1 w-1/2">
<div className="capitalize font-semibold text-sm">
{getTimelineItemDescription(timeline)}
</div>
<div className="text-sm">
{formatUnixTimestampToDateTime(timeline.timestamp, {
strftime_fmt:
config?.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S %p",
time_style: "medium",
date_style: "medium",
})}
</div>
<Button
className="absolute bottom-1 right-1"
size="sm"
variant="secondary"
>
<div className="w-8 h-8">
<Logo />
</div>
+
</Button>
</div>
</Card>
);
}

View File

@@ -186,8 +186,8 @@ function PreviewContent({
const touchEnd = new Date().getTime();
// consider tap less than 500 ms
if (touchEnd - touchStart < 500) {
// consider tap less than 300 ms
if (touchEnd - touchStart < 300) {
onClick();
}
});
@@ -214,10 +214,11 @@ function PreviewContent({
} else {
return (
<>
<div className={`${getPreviewWidth(camera, config)}`}>
<div className="w-full">
<VideoPlayer
options={{
preload: "auto",
aspectRatio: "16:9",
autoplay: true,
controls: false,
muted: true,
@@ -263,12 +264,12 @@ function isCurrentHour(timestamp: number) {
function getPreviewWidth(camera: string, config: FrigateConfig) {
const detect = config.cameras[camera].detect;
if (detect.width / detect.height < 1.0) {
return "w-[120px]";
if (detect.width / detect.height < 1) {
return "w-1/2";
}
if (detect.width / detect.height < 1.4) {
return "w-[208px]";
if (detect.width / detect.height < 16 / 9) {
return "w-2/3";
}
return "w-full";

View File

@@ -76,7 +76,7 @@ const domEvents: TimelineEventsWithMissing[] = [
type ActivityScrubberProps = {
className?: string;
items?: TimelineItem[];
timeBars?: { time: DateType; id?: IdType | undefined }[];
timeBars?: { time: DateType; id: IdType }[];
groups?: TimelineGroup[];
options?: TimelineOptions;
} & TimelineEventsHandlers;
@@ -94,6 +94,9 @@ function ActivityScrubber({
timeline: null,
});
const [currentTime, setCurrentTime] = useState(Date.now());
const [_, setCustomTimes] = useState<
{ id: IdType; time: DateType }[]
>([]);
const defaultOptions: TimelineOptions = {
width: "100%",
@@ -161,6 +164,41 @@ function ActivityScrubber({
};
}, [containerRef]);
// need to keep custom times in sync
useEffect(() => {
if (!timelineRef.current.timeline || timeBars == undefined) {
return;
}
setCustomTimes((prevTimes) => {
if (prevTimes.length == 0 && timeBars.length == 0) {
return [];
}
prevTimes
.filter((x) => timeBars.find((y) => x.id == y.id) == undefined)
.forEach((time) => {
try {
timelineRef.current.timeline?.removeCustomTime(time.id);
} catch {}
});
timeBars.forEach((time) => {
try {
const existing = timelineRef.current.timeline?.getCustomTime(time.id);
if (existing != time.time) {
timelineRef.current.timeline?.setCustomTime(time.time, time.id);
}
} catch {
timelineRef.current.timeline?.addCustomTime(time.time, time.id);
}
});
return timeBars;
});
}, [timeBars, timelineRef]);
return (
<div className={className || ""}>
<div ref={containerRef} />