forked from Github/frigate
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:
committed by
Blake Blackshear
parent
0ee81c7526
commit
160e331035
76
web/src/components/card/TimelineItemCard.tsx
Normal file
76
web/src/components/card/TimelineItemCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user