mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 01:05:20 +03:00
refactor: rewrote logic for building timeline to support stacking and changed to typescript
This commit is contained in:
parent
7401789b3b
commit
ddf7065379
@ -7,7 +7,7 @@ import { Play } from '../icons/Play';
|
|||||||
import { Previous } from '../icons/Previous';
|
import { Previous } from '../icons/Previous';
|
||||||
import { HistoryHeader } from '../routes/HistoryHeader';
|
import { HistoryHeader } from '../routes/HistoryHeader';
|
||||||
import { longToDate } from '../utils/dateUtil';
|
import { longToDate } from '../utils/dateUtil';
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline/Timeline';
|
||||||
|
|
||||||
const getLast24Hours = () => {
|
const getLast24Hours = () => {
|
||||||
return new Number(new Date(new Date().getTime() - 24 * 60 * 60 * 1000)) / 1000;
|
return new Number(new Date(new Date().getTime() - 24 * 60 * 60 * 1000)) / 1000;
|
||||||
@ -51,10 +51,7 @@ export default function HistoryViewer({ camera }) {
|
|||||||
|
|
||||||
const handleTimelineChange = (timelineChangedEvent) => {
|
const handleTimelineChange = (timelineChangedEvent) => {
|
||||||
if (timelineChangedEvent.seekComplete) {
|
if (timelineChangedEvent.seekComplete) {
|
||||||
const currentEventExists = currentEvent !== undefined;
|
setCurrentEvent(timelineChangedEvent.event);
|
||||||
if (!currentEventExists || currentEvent.id !== timelineChangedEvent.event.id) {
|
|
||||||
setCurrentEvent(timelineChangedEvent.event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoContainer = videoRef.current;
|
const videoContainer = videoRef.current;
|
||||||
@ -143,7 +140,8 @@ export default function HistoryViewer({ camera }) {
|
|||||||
<div className='relative flex flex-col'>
|
<div className='relative flex flex-col'>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
camera={camera}
|
camera={camera}
|
||||||
date={longToDate(currentEvent.start_time)}
|
date={currentEvent.startTime}
|
||||||
|
end={currentEvent.endTime}
|
||||||
objectLabel={currentEvent.label}
|
objectLabel={currentEvent.label}
|
||||||
className='mb-2'
|
className='mb-2'
|
||||||
/>
|
/>
|
||||||
@ -156,7 +154,7 @@ export default function HistoryViewer({ camera }) {
|
|||||||
events={timelineEvents}
|
events={timelineEvents}
|
||||||
offset={timelineOffset}
|
offset={timelineOffset}
|
||||||
currentIndex={currentEventIndex}
|
currentIndex={currentEventIndex}
|
||||||
disabled={isPlaying}
|
disableMarkerEvents={isPlaying}
|
||||||
onChange={handleTimelineChange}
|
onChange={handleTimelineChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -1,217 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
import { longToDate } from '../utils/dateUtil';
|
|
||||||
|
|
||||||
export default function Timeline({ events, offset, currentIndex, disabled, onChange }) {
|
|
||||||
const timelineContainerRef = useRef(undefined);
|
|
||||||
|
|
||||||
const [timeline, setTimeline] = useState([]);
|
|
||||||
const [timelineOffset, setTimelineOffset] = useState();
|
|
||||||
const [markerTime, setMarkerTime] = useState();
|
|
||||||
const [currentEvent, setCurrentEvent] = useState();
|
|
||||||
const [scrollTimeout, setScrollTimeout] = useState();
|
|
||||||
const [scrollActive, setScrollActive] = useState(true);
|
|
||||||
const [eventsEnabled, setEventsEnable] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (events && events.length > 0 && timelineOffset) {
|
|
||||||
const firstEvent = events[0];
|
|
||||||
if (firstEvent) {
|
|
||||||
setMarkerTime(longToDate(firstEvent.start_time));
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstEventTime = longToDate(firstEvent.start_time);
|
|
||||||
const timelineEvents = events.map((e, i) => {
|
|
||||||
const startTime = longToDate(e.start_time);
|
|
||||||
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
|
|
||||||
const seconds = Math.round(Math.abs(endTime - startTime) / 1000);
|
|
||||||
const positionX = Math.round(Math.abs(startTime - firstEventTime) / 1000 + timelineOffset);
|
|
||||||
return {
|
|
||||||
...e,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
seconds,
|
|
||||||
width: seconds,
|
|
||||||
positionX,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstTimelineEvent = timelineEvents[0];
|
|
||||||
setCurrentEvent({
|
|
||||||
...firstTimelineEvent,
|
|
||||||
id: firstTimelineEvent.id,
|
|
||||||
index: 0,
|
|
||||||
startTime: longToDate(firstTimelineEvent.start_time),
|
|
||||||
endTime: longToDate(firstTimelineEvent.end_time),
|
|
||||||
});
|
|
||||||
setTimeline(timelineEvents);
|
|
||||||
}
|
|
||||||
}, [events, timelineOffset]);
|
|
||||||
|
|
||||||
const getCurrentEvent = useCallback(() => {
|
|
||||||
return currentEvent;
|
|
||||||
}, [currentEvent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cEvent = getCurrentEvent();
|
|
||||||
if (cEvent && offset >= 0) {
|
|
||||||
timelineContainerRef.current.scroll({
|
|
||||||
left: cEvent.positionX + offset - timelineOffset,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [offset, timelineContainerRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timeline.length > 0 && currentIndex !== undefined) {
|
|
||||||
const event = timeline[currentIndex];
|
|
||||||
setCurrentEvent({
|
|
||||||
...event,
|
|
||||||
id: event.id,
|
|
||||||
index: currentIndex,
|
|
||||||
startTime: longToDate(event.start_time),
|
|
||||||
endTime: longToDate(event.end_time),
|
|
||||||
});
|
|
||||||
timelineContainerRef.current.scroll({ left: event.positionX - timelineOffset, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, [currentIndex, timelineContainerRef, timeline]);
|
|
||||||
|
|
||||||
const checkMarkerForEvent = (markerTime) => {
|
|
||||||
if (!scrollActive) {
|
|
||||||
setScrollActive(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeline) {
|
|
||||||
const foundIndex = timeline.findIndex((event) => event.startTime <= markerTime && markerTime <= event.endTime);
|
|
||||||
if (foundIndex > -1) {
|
|
||||||
const found = timeline[foundIndex];
|
|
||||||
if (found !== currentEvent && found.id !== currentEvent.id) {
|
|
||||||
setCurrentEvent({
|
|
||||||
...found,
|
|
||||||
id: found.id,
|
|
||||||
index: foundIndex,
|
|
||||||
startTime: longToDate(found.start_time),
|
|
||||||
endTime: longToDate(found.end_time),
|
|
||||||
});
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollLogic = () => {
|
|
||||||
clearTimeout(scrollTimeout);
|
|
||||||
|
|
||||||
const scrollPosition = timelineContainerRef.current.scrollLeft;
|
|
||||||
const startTime = longToDate(timeline[0].start_time);
|
|
||||||
const markerTime = new Date(startTime.getTime() + scrollPosition * 1000);
|
|
||||||
setMarkerTime(markerTime);
|
|
||||||
|
|
||||||
handleChange(currentEvent, markerTime, false);
|
|
||||||
|
|
||||||
setScrollTimeout(
|
|
||||||
setTimeout(() => {
|
|
||||||
const foundEvent = checkMarkerForEvent(markerTime);
|
|
||||||
handleChange(foundEvent ? foundEvent : currentEvent, markerTime, true);
|
|
||||||
}, 250)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (event) => {
|
|
||||||
if (!disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollLogic(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = (event) => {
|
|
||||||
if (disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scrollLogic(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timelineContainerRef) {
|
|
||||||
const timelineContainerWidth = timelineContainerRef.current.offsetWidth;
|
|
||||||
const offset = Math.round(timelineContainerWidth / 2);
|
|
||||||
setTimelineOffset(offset);
|
|
||||||
}
|
|
||||||
}, [timelineContainerRef]);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(event, time, seekComplete) => {
|
|
||||||
if (onChange !== undefined) {
|
|
||||||
onChange({
|
|
||||||
event,
|
|
||||||
time,
|
|
||||||
seekComplete,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const RenderTimeline = useCallback(() => {
|
|
||||||
if (timeline && timeline.length > 0) {
|
|
||||||
const lastEvent = timeline[timeline.length - 1];
|
|
||||||
const timelineLength = timelineOffset + lastEvent.positionX + lastEvent.width;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='relative flex items-center h-20'
|
|
||||||
style={{
|
|
||||||
width: `${timelineLength}px`,
|
|
||||||
background: "url('/marker.png')",
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
backgroundSize: '30px',
|
|
||||||
backgroundRepeat: 'repeat-x',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{timeline.map((e) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={e.id}
|
|
||||||
className='absolute z-10 rounded-full bg-blue-300 h-2'
|
|
||||||
style={{
|
|
||||||
left: `${e.positionX}px`,
|
|
||||||
width: `${e.seconds}px`,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [timeline, timelineOffset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative flex-grow-1'>
|
|
||||||
<div className='absolute left-0 top-0 h-full w-full' style={{ textAlign: 'center' }}>
|
|
||||||
<div className='h-full' style={{ margin: '0 auto', textAlign: 'center' }}>
|
|
||||||
<span className='z-20 text-black dark:text-white'>
|
|
||||||
{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className='z-20 h-full absolute'
|
|
||||||
style={{
|
|
||||||
left: 'calc(100% / 2)',
|
|
||||||
height: 'calc(100% - 24px)',
|
|
||||||
borderRight: '2px solid rgba(252, 211, 77)',
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={timelineContainerRef}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
onTouchMove={handleWheel}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
className='overflow-x-auto hide-scroll'
|
|
||||||
>
|
|
||||||
<RenderTimeline />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
235
web/src/components/Timeline/Timeline.tsx
Normal file
235
web/src/components/Timeline/Timeline.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { longToDate } from '../../utils/dateUtil';
|
||||||
|
import { TimelineBlocks } from './TimelineBlocks';
|
||||||
|
|
||||||
|
export interface TimelineEvent {
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineEventBlock extends TimelineEvent {
|
||||||
|
index: number;
|
||||||
|
yOffset: number;
|
||||||
|
width: number;
|
||||||
|
positionX: number;
|
||||||
|
seconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimelineProps {
|
||||||
|
events: TimelineEvent[];
|
||||||
|
offset: number;
|
||||||
|
disableMarkerEvents?: boolean;
|
||||||
|
onChange: (timelineChangedEvent: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Timeline({ events, offset, disableMarkerEvents, onChange }: TimelineProps) {
|
||||||
|
const timelineContainerRef = useRef<HTMLDivElement>(undefined);
|
||||||
|
|
||||||
|
const [timeline, setTimeline] = useState<TimelineEventBlock[]>([]);
|
||||||
|
const [timelineOffset, setTimelineOffset] = useState<number | undefined>(undefined);
|
||||||
|
const [markerTime, setMarkerTime] = useState<Date | undefined>(undefined);
|
||||||
|
const [currentEvent, setCurrentEvent] = useState<TimelineEventBlock | undefined>(undefined);
|
||||||
|
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const [isScrollAllowed, setScrollAllowed] = useState(!disableMarkerEvents);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScrollAllowed(!disableMarkerEvents);
|
||||||
|
}, [disableMarkerEvents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (offset > 0) {
|
||||||
|
scrollToPositionInCurrentEvent(offset);
|
||||||
|
}
|
||||||
|
}, [offset]);
|
||||||
|
|
||||||
|
const checkEventForOverlap = (firstEvent: TimelineEvent, secondEvent: TimelineEvent) => {
|
||||||
|
if (secondEvent.startTime < firstEvent.endTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const determineOffset = (currentEvent: TimelineEventBlock, previousEvents: TimelineEventBlock[]): number => {
|
||||||
|
const OFFSET_DISTANCE_IN_PIXELS = 10;
|
||||||
|
const previousIndex = previousEvents.length - 1;
|
||||||
|
const previousEvent = previousEvents[previousIndex];
|
||||||
|
if (previousEvent) {
|
||||||
|
const isOverlap = checkEventForOverlap(previousEvent, currentEvent);
|
||||||
|
if (isOverlap) {
|
||||||
|
return OFFSET_DISTANCE_IN_PIXELS + determineOffset(currentEvent, previousEvents.slice(0, previousIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTimelineView = (events: TimelineEvent[]): TimelineEventBlock[] => {
|
||||||
|
const firstEvent = events[0];
|
||||||
|
const firstEventTime = longToDate(firstEvent.start_time);
|
||||||
|
return events
|
||||||
|
.map((e, index) => {
|
||||||
|
const startTime = longToDate(e.start_time);
|
||||||
|
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
|
||||||
|
const seconds = Math.round(Math.abs(endTime.getTime() - startTime.getTime()) / 1000);
|
||||||
|
const positionX = Math.round(Math.abs(startTime.getTime() - firstEventTime.getTime()) / 1000 + timelineOffset);
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
width: seconds,
|
||||||
|
positionX,
|
||||||
|
index,
|
||||||
|
} as TimelineEventBlock;
|
||||||
|
})
|
||||||
|
.reduce((eventBlocks, current) => {
|
||||||
|
const offset = determineOffset(current, eventBlocks);
|
||||||
|
current.yOffset = offset;
|
||||||
|
return [...eventBlocks, current];
|
||||||
|
}, [] as TimelineEventBlock[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (events && events.length > 0 && timelineOffset) {
|
||||||
|
const timelineEvents = buildTimelineView(events);
|
||||||
|
const lastEventIndex = timelineEvents.length - 1;
|
||||||
|
|
||||||
|
setTimeline(timelineEvents);
|
||||||
|
setMarkerTime(timelineEvents[lastEventIndex].startTime);
|
||||||
|
setCurrentEvent(timelineEvents[lastEventIndex]);
|
||||||
|
}
|
||||||
|
}, [events, timelineOffset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timelineIsLoaded = timeline.length > 0;
|
||||||
|
if (timelineIsLoaded) {
|
||||||
|
const lastEvent = timeline[timeline.length - 1];
|
||||||
|
scrollToEvent(lastEvent);
|
||||||
|
}
|
||||||
|
}, [timeline]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentEvent) {
|
||||||
|
handleChange(true);
|
||||||
|
}
|
||||||
|
}, [currentEvent]);
|
||||||
|
|
||||||
|
const disableScrollEvent = (milliseconds) => {
|
||||||
|
setScrollAllowed(false);
|
||||||
|
let timeout: NodeJS.Timeout = undefined;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setScrollAllowed(true);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, milliseconds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToPosition = (positionX: number) => {
|
||||||
|
if (timelineContainerRef.current) {
|
||||||
|
disableScrollEvent(150);
|
||||||
|
timelineContainerRef.current.scroll({
|
||||||
|
left: positionX,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToPositionInCurrentEvent = (offset: number) => {
|
||||||
|
scrollToPosition(currentEvent.positionX + offset - timelineOffset);
|
||||||
|
setMarkerTime(getCurrentMarkerTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToEvent = (event, offset = 0) => {
|
||||||
|
scrollToPosition(event.positionX + offset - timelineOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkMarkerForEvent = (markerTime) => {
|
||||||
|
return [...timeline]
|
||||||
|
.reverse()
|
||||||
|
.find((timelineEvent) => timelineEvent.startTime <= markerTime && timelineEvent.endTime >= markerTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentMarkerTime = () => {
|
||||||
|
if (timelineContainerRef.current && timeline.length > 0) {
|
||||||
|
const scrollPosition = timelineContainerRef.current.scrollLeft;
|
||||||
|
const firstTimelineEvent = timeline[0] as TimelineEventBlock;
|
||||||
|
const firstTimelineEventStartTime = firstTimelineEvent.startTime.getTime();
|
||||||
|
return new Date(firstTimelineEventStartTime + scrollPosition * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimelineScrollHandler = () => {
|
||||||
|
if (isScrollAllowed) {
|
||||||
|
if (timelineContainerRef.current && timeline.length > 0) {
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
|
const currentMarkerTime = getCurrentMarkerTime();
|
||||||
|
setMarkerTime(currentMarkerTime);
|
||||||
|
handleChange(false);
|
||||||
|
|
||||||
|
setScrollTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
const overlappingEvent = checkMarkerForEvent(currentMarkerTime);
|
||||||
|
setCurrentEvent(overlappingEvent);
|
||||||
|
}, 150)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timelineContainerRef) {
|
||||||
|
const timelineContainerWidth = timelineContainerRef.current.offsetWidth;
|
||||||
|
const offset = Math.round(timelineContainerWidth / 2);
|
||||||
|
setTimelineOffset(offset);
|
||||||
|
}
|
||||||
|
}, [timelineContainerRef]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(seekComplete: boolean) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange({
|
||||||
|
event: currentEvent,
|
||||||
|
markerTime,
|
||||||
|
seekComplete,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, currentEvent, markerTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleViewEvent = (event: TimelineEventBlock) => {
|
||||||
|
setCurrentEvent(event);
|
||||||
|
setMarkerTime(getCurrentMarkerTime());
|
||||||
|
scrollToEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-grow-1'>
|
||||||
|
<div className='w-full text-center'>
|
||||||
|
<span className='text-black dark:text-white'>
|
||||||
|
{markerTime && <span>{markerTime.toLocaleTimeString()}</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='relative'>
|
||||||
|
<div className='absolute left-0 top-0 h-full w-full text-center'>
|
||||||
|
<div className='h-full text-center' style={{ margin: '0 auto' }}>
|
||||||
|
<div
|
||||||
|
className='z-20 h-full absolute'
|
||||||
|
style={{
|
||||||
|
left: 'calc(100% / 2)',
|
||||||
|
borderRight: '2px solid rgba(252, 211, 77)',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={timelineContainerRef} onScroll={onTimelineScrollHandler} className='overflow-x-auto hide-scroll'>
|
||||||
|
{timeline.length > 0 && (
|
||||||
|
<TimelineBlocks timeline={timeline} firstBlockOffset={timelineOffset} onEventClick={handleViewEvent} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
web/src/components/Timeline/TimelineBlockView.tsx
Normal file
24
web/src/components/Timeline/TimelineBlockView.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { TimelineEventBlock } from './Timeline';
|
||||||
|
|
||||||
|
interface TimelineBlockViewProps {
|
||||||
|
block: TimelineEventBlock;
|
||||||
|
onClick: (block: TimelineEventBlock) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TimelineBlockView = ({ block, onClick }: TimelineBlockViewProps) => {
|
||||||
|
const onClickHandler = () => onClick(block);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={block.id}
|
||||||
|
onClick={onClickHandler}
|
||||||
|
className='absolute z-10 rounded-full bg-blue-300 h-2'
|
||||||
|
style={{
|
||||||
|
top: `${block.yOffset}px`,
|
||||||
|
left: `${block.positionX}px`,
|
||||||
|
width: `${block.width}px`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
39
web/src/components/Timeline/TimelineBlocks.tsx
Normal file
39
web/src/components/Timeline/TimelineBlocks.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { TimelineEventBlock } from './Timeline';
|
||||||
|
import { TimelineBlockView } from './TimelineBlockView';
|
||||||
|
|
||||||
|
interface TimelineBlocksProps {
|
||||||
|
timeline: TimelineEventBlock[];
|
||||||
|
firstBlockOffset: number;
|
||||||
|
onEventClick: (block: TimelineEventBlock) => void;
|
||||||
|
}
|
||||||
|
export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: TimelineBlocksProps) => {
|
||||||
|
const calculateTimelineContainerWidth = () => {
|
||||||
|
if (timeline.length > 0) {
|
||||||
|
const startTimeEpoch = timeline[0].startTime.getTime();
|
||||||
|
const endTimeEpoch = new Date().getTime();
|
||||||
|
return Math.round(Math.abs(endTimeEpoch - startTimeEpoch) / 1000) + firstBlockOffset * 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickHandler = (block: TimelineEventBlock) => onEventClick(block);
|
||||||
|
|
||||||
|
if (timeline && timeline.length > 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='relative block whitespace-nowrap overflow-x-hidden h-16'
|
||||||
|
style={{
|
||||||
|
width: `${calculateTimelineContainerWidth()}px`,
|
||||||
|
background: "url('/marker.png')",
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundSize: '30px',
|
||||||
|
backgroundRepeat: 'repeat',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timeline.map((block) => (
|
||||||
|
<TimelineBlockView block={block} onClick={onClickHandler} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact';
|
||||||
import Heading from '../components/Heading'
|
import Heading from '../components/Heading';
|
||||||
|
|
||||||
export function HistoryHeader({ objectLabel, date, camera, className }) {
|
export function HistoryHeader({ objectLabel, date, end, camera, className }) {
|
||||||
return (
|
return (
|
||||||
<div className={`text-center ${className}`}>
|
<div className={`text-center ${className}`}>
|
||||||
<Heading size="lg">{objectLabel}</Heading>
|
<Heading size='lg'>{objectLabel}</Heading>
|
||||||
<div>
|
<div>
|
||||||
<span>Today, {date.toLocaleTimeString()} · {camera}</span>
|
<span>
|
||||||
|
Today, {date.toLocaleTimeString()} - {end ? end.toLocaleTimeString() : 'Incomplete'} · {camera}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
7
web/tsconfig.json
Normal file
7
web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxFactory": "h",
|
||||||
|
"jsxFragmentFactory": "Fragment",
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user