mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
UI fixes (#9986)
* scroll minimap to keep it in view * remove console log * change ref * rebase to dev * rebase to dev * rebase to dev * fix history flexbox and live extra scrollbar * remove extra class
This commit is contained in:
parent
f84d2db406
commit
a6aa5328aa
@ -1,7 +1,7 @@
|
|||||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||||
import { useSegmentUtils } from "@/hooks/use-segment-utils";
|
import { useSegmentUtils } from "@/hooks/use-segment-utils";
|
||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import React, { useMemo } from "react";
|
import React, { useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
type EventSegmentProps = {
|
type EventSegmentProps = {
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
@ -19,6 +19,7 @@ type MinimapSegmentProps = {
|
|||||||
isLastSegmentInMinimap: boolean;
|
isLastSegmentInMinimap: boolean;
|
||||||
alignedMinimapStartTime: number;
|
alignedMinimapStartTime: number;
|
||||||
alignedMinimapEndTime: number;
|
alignedMinimapEndTime: number;
|
||||||
|
firstMinimapSegmentRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TickSegmentProps = {
|
type TickSegmentProps = {
|
||||||
@ -41,11 +42,15 @@ function MinimapBounds({
|
|||||||
isLastSegmentInMinimap,
|
isLastSegmentInMinimap,
|
||||||
alignedMinimapStartTime,
|
alignedMinimapStartTime,
|
||||||
alignedMinimapEndTime,
|
alignedMinimapEndTime,
|
||||||
|
firstMinimapSegmentRef,
|
||||||
}: MinimapSegmentProps) {
|
}: MinimapSegmentProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isFirstSegmentInMinimap && (
|
{isFirstSegmentInMinimap && (
|
||||||
<div className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px]">
|
<div
|
||||||
|
className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px] scroll-mt-8"
|
||||||
|
ref={firstMinimapSegmentRef}
|
||||||
|
>
|
||||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -179,6 +184,19 @@ export function EventSegment({
|
|||||||
return showMinimap && segmentTime === alignedMinimapEndTime;
|
return showMinimap && segmentTime === alignedMinimapEndTime;
|
||||||
}, [showMinimap, segmentTime, alignedMinimapEndTime]);
|
}, [showMinimap, segmentTime, alignedMinimapEndTime]);
|
||||||
|
|
||||||
|
const firstMinimapSegmentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if the first segment is out of view
|
||||||
|
const firstSegment = firstMinimapSegmentRef.current;
|
||||||
|
if (firstSegment && showMinimap && isFirstSegmentInMinimap) {
|
||||||
|
firstSegment.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]);
|
||||||
|
|
||||||
const segmentClasses = `flex flex-row ${
|
const segmentClasses = `flex flex-row ${
|
||||||
showMinimap
|
showMinimap
|
||||||
? isInMinimapRange
|
? isInMinimapRange
|
||||||
@ -212,6 +230,7 @@ export function EventSegment({
|
|||||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||||
alignedMinimapStartTime={alignedMinimapStartTime}
|
alignedMinimapStartTime={alignedMinimapStartTime}
|
||||||
alignedMinimapEndTime={alignedMinimapEndTime}
|
alignedMinimapEndTime={alignedMinimapEndTime}
|
||||||
|
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tick
|
<Tick
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
interface DragHandlerProps {
|
interface DragHandlerProps {
|
||||||
contentRef: React.RefObject<HTMLElement>;
|
contentRef: React.RefObject<HTMLElement>;
|
||||||
@ -128,6 +128,17 @@ function useDraggableHandler({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: determine when we want to do this
|
||||||
|
const handlebar = scrollTimeRef.current;
|
||||||
|
if (handlebar && showHandlebar) {
|
||||||
|
// handlebar.scrollIntoView({
|
||||||
|
// behavior: "smooth",
|
||||||
|
// block: "center",
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ function Live() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-scroll px-2">
|
<div className="w-full h-full overflow-y-scroll px-2">
|
||||||
{events && events.length > 0 && (
|
{events && events.length > 0 && (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
@ -61,7 +61,8 @@ function eventsToScrubberItems(events: Event[]): ScrubberItem[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generateRandomEvent = (): ReviewSegment => {
|
const generateRandomEvent = (): ReviewSegment => {
|
||||||
const start_time = Math.floor(Date.now() / 1000) - Math.random() * 60 * 60;
|
const start_time =
|
||||||
|
Math.floor(Date.now() / 1000) - 10800 - Math.random() * 60 * 60;
|
||||||
const end_time = Math.floor(start_time + Math.random() * 60 * 10);
|
const end_time = Math.floor(start_time + Math.random() * 60 * 10);
|
||||||
const severities: ReviewSeverity[] = [
|
const severities: ReviewSeverity[] = [
|
||||||
"significant_motion",
|
"significant_motion",
|
||||||
@ -123,6 +124,23 @@ function UIPlayground() {
|
|||||||
setMockEvents(initialEvents);
|
setMockEvents(initialEvents);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Calculate minimap start and end times based on events
|
||||||
|
const minimapStartTime = useMemo(() => {
|
||||||
|
if (mockEvents && mockEvents.length > 0) {
|
||||||
|
return Math.min(...mockEvents.map((event) => event.start_time));
|
||||||
|
}
|
||||||
|
return Math.floor(Date.now() / 1000); // Default to current time if no events
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
const minimapEndTime = useMemo(() => {
|
||||||
|
if (mockEvents && mockEvents.length > 0) {
|
||||||
|
return Math.max(
|
||||||
|
...mockEvents.map((event) => event.end_time ?? event.start_time)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.floor(Date.now() / 1000); // Default to current time if no events
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
const [zoomLevel, setZoomLevel] = useState(0);
|
const [zoomLevel, setZoomLevel] = useState(0);
|
||||||
const [zoomSettings, setZoomSettings] = useState({
|
const [zoomSettings, setZoomSettings] = useState({
|
||||||
segmentDuration: 60,
|
segmentDuration: 60,
|
||||||
@ -150,101 +168,114 @@ function UIPlayground() {
|
|||||||
setZoomSettings(possibleZoomLevels[nextZoomLevel]);
|
setZoomSettings(possibleZoomLevels[nextZoomLevel]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
const handleDraggingChange = (dragging: boolean) => {
|
||||||
|
setIsDragging(dragging);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading as="h2">UI Playground</Heading>
|
<div className="w-full h-full">
|
||||||
|
<div className="flex h-full">
|
||||||
|
<div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
|
||||||
|
<Heading as="h2">UI Playground</Heading>
|
||||||
|
|
||||||
<Heading as="h4" className="my-5">
|
|
||||||
Scrubber
|
|
||||||
</Heading>
|
|
||||||
<p className="text-small">
|
|
||||||
Shows the 10 most recent events within the last 4 hours
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{!config && <ActivityIndicator />}
|
|
||||||
|
|
||||||
{config && (
|
|
||||||
<div>
|
|
||||||
{events && events.length > 0 && (
|
|
||||||
<>
|
|
||||||
<ActivityScrubber
|
|
||||||
items={eventsToScrubberItems(events)}
|
|
||||||
selectHandler={onSelect}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{config && (
|
|
||||||
<div>
|
|
||||||
{timeline && (
|
|
||||||
<>
|
|
||||||
<TimelineScrubber eventID={timeline} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<div ref={contentRef}>
|
|
||||||
<Heading as="h4" className="my-5">
|
<Heading as="h4" className="my-5">
|
||||||
Timeline
|
Scrubber
|
||||||
</Heading>
|
|
||||||
<p className="text-small">Handlebar timestamp: {handlebarTime}</p>
|
|
||||||
<p>
|
|
||||||
<Button onClick={handleZoomOut} disabled={zoomLevel === 0}>
|
|
||||||
Zoom Out
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleZoomIn}
|
|
||||||
disabled={zoomLevel === possibleZoomLevels.length - 1}
|
|
||||||
>
|
|
||||||
Zoom In
|
|
||||||
</Button>
|
|
||||||
</p>
|
|
||||||
<Heading as="h4" className="my-5">
|
|
||||||
Color scheme
|
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-small">
|
<p className="text-small">
|
||||||
Colors as set by the current theme. See the{" "}
|
Shows the 10 most recent events within the last 4 hours
|
||||||
<a
|
|
||||||
className="underline"
|
|
||||||
href="https://ui.shadcn.com/docs/theming"
|
|
||||||
>
|
|
||||||
shadcn theming docs
|
|
||||||
</a>{" "}
|
|
||||||
for usage.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="my-5">
|
{!config && <ActivityIndicator />}
|
||||||
{colors.map((color, index) => (
|
|
||||||
<ColorSwatch
|
{config && (
|
||||||
key={index}
|
<div>
|
||||||
name={color}
|
{events && events.length > 0 && (
|
||||||
value={`hsl(var(--${color}))`}
|
<>
|
||||||
/>
|
<ActivityScrubber
|
||||||
))}
|
items={eventsToScrubberItems(events)}
|
||||||
|
selectHandler={onSelect}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config && (
|
||||||
|
<div>
|
||||||
|
{timeline && (
|
||||||
|
<>
|
||||||
|
<TimelineScrubber eventID={timeline} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={contentRef}>
|
||||||
|
<Heading as="h4" className="my-5">
|
||||||
|
Timeline
|
||||||
|
</Heading>
|
||||||
|
<p className="text-small">Handlebar timestamp: {handlebarTime}</p>
|
||||||
|
<p className="text-small">
|
||||||
|
Handlebar is dragging: {isDragging ? "yes" : "no"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Button onClick={handleZoomOut} disabled={zoomLevel === 0}>
|
||||||
|
Zoom Out
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
disabled={zoomLevel === possibleZoomLevels.length - 1}
|
||||||
|
>
|
||||||
|
Zoom In
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
<Heading as="h4" className="my-5">
|
||||||
|
Color scheme
|
||||||
|
</Heading>
|
||||||
|
<p className="text-small">
|
||||||
|
Colors as set by the current theme. See the{" "}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://ui.shadcn.com/docs/theming"
|
||||||
|
>
|
||||||
|
shadcn theming docs
|
||||||
|
</a>{" "}
|
||||||
|
for usage.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="my-5">
|
||||||
|
{colors.map((color, index) => (
|
||||||
|
<ColorSwatch
|
||||||
|
key={index}
|
||||||
|
name={color}
|
||||||
|
value={`hsl(var(--${color}))`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex-none">
|
<div className="w-[100px] overflow-y-auto no-scrollbar">
|
||||||
<EventReviewTimeline
|
<EventReviewTimeline
|
||||||
segmentDuration={zoomSettings.segmentDuration} // seconds per segment
|
segmentDuration={zoomSettings.segmentDuration} // seconds per segment
|
||||||
timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
|
timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
|
||||||
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
|
timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
|
||||||
timelineEnd={Math.floor(Date.now() / 1000) - 6 * 60 * 60} // end of timeline - the later time
|
timelineEnd={Math.floor(Date.now() / 1000) - 6 * 60 * 60} // end of timeline - the later time
|
||||||
showHandlebar // show / hide the handlebar
|
showHandlebar // show / hide the handlebar
|
||||||
handlebarTime={handlebarTime} // set the time of the handlebar
|
handlebarTime={handlebarTime} // set the time of the handlebar
|
||||||
setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
|
setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
|
||||||
showMinimap // show / hide the minimap
|
onHandlebarDraggingChange={handleDraggingChange} // function for state of handlebar dragging
|
||||||
minimapStartTime={Math.floor(Date.now() / 1000) - 35 * 60} // start time of the minimap - the earlier time (eg 1:00pm)
|
showMinimap // show / hide the minimap
|
||||||
minimapEndTime={Math.floor(Date.now() / 1000) - 21 * 60} // end of the minimap - the later time (eg 3:00pm)
|
minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
|
||||||
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
|
||||||
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
events={mockEvents} // events, including new has_been_reviewed and severity properties
|
||||||
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
|
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
|
||||||
/>
|
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -195,8 +195,8 @@ export default function DesktopEventView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
<div className="absolute flex justify-between left-0 top-0 right-0">
|
<div className="flex justify-between mb-2">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
defaultValue="alert"
|
defaultValue="alert"
|
||||||
@ -261,55 +261,59 @@ export default function DesktopEventView({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div className="flex h-full overflow-hidden">
|
||||||
ref={contentRef}
|
<div
|
||||||
className="absolute left-0 top-12 bottom-0 right-28 flex flex-wrap content-start gap-2 overflow-y-auto no-scrollbar"
|
ref={contentRef}
|
||||||
>
|
className="flex flex-1 flex-wrap content-start gap-2 overflow-y-auto no-scrollbar"
|
||||||
{currentItems ? (
|
>
|
||||||
currentItems.map((value, segIdx) => {
|
{currentItems ? (
|
||||||
const lastRow = segIdx == reviewItems[severity].length - 1;
|
currentItems.map((value, segIdx) => {
|
||||||
const relevantPreview = Object.values(relevantPreviews || []).find(
|
const lastRow = segIdx == reviewItems[severity].length - 1;
|
||||||
(preview) =>
|
const relevantPreview = Object.values(
|
||||||
preview.camera == value.camera &&
|
relevantPreviews || []
|
||||||
preview.start < value.start_time &&
|
).find(
|
||||||
preview.end > value.end_time
|
(preview) =>
|
||||||
);
|
preview.camera == value.camera &&
|
||||||
|
preview.start < value.start_time &&
|
||||||
|
preview.end > value.end_time
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={value.id}
|
key={value.id}
|
||||||
ref={lastRow ? lastReviewRef : minimapRef}
|
ref={lastRow ? lastReviewRef : minimapRef}
|
||||||
data-start={value.start_time}
|
data-start={value.start_time}
|
||||||
>
|
>
|
||||||
<div className="h-[234px] aspect-video rounded-lg overflow-hidden">
|
<div className="h-[234px] aspect-video rounded-lg overflow-hidden">
|
||||||
<PreviewThumbnailPlayer
|
<PreviewThumbnailPlayer
|
||||||
review={value}
|
review={value}
|
||||||
relevantPreview={relevantPreview}
|
relevantPreview={relevantPreview}
|
||||||
setReviewed={() => markItemAsReviewed(value.id)}
|
setReviewed={() => markItemAsReviewed(value.id)}
|
||||||
onClick={() => onSelectReview(value.id)}
|
onClick={() => onSelectReview(value.id)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
{lastRow && !reachedEnd && <ActivityIndicator />}
|
||||||
</div>
|
</div>
|
||||||
{lastRow && !reachedEnd && <ActivityIndicator />}
|
);
|
||||||
</div>
|
})
|
||||||
);
|
) : (
|
||||||
})
|
<div ref={lastReviewRef} />
|
||||||
) : (
|
)}
|
||||||
<div ref={lastReviewRef} />
|
</div>
|
||||||
)}
|
<div className="md:w-[100px] overflow-y-auto no-scrollbar">
|
||||||
</div>
|
<EventReviewTimeline
|
||||||
<div className="absolute top-12 right-0 bottom-0">
|
segmentDuration={60}
|
||||||
<EventReviewTimeline
|
timestampSpread={15}
|
||||||
segmentDuration={60}
|
timelineStart={timeRange.before}
|
||||||
timestampSpread={15}
|
timelineEnd={timeRange.after}
|
||||||
timelineStart={timeRange.before}
|
showMinimap
|
||||||
timelineEnd={timeRange.after}
|
minimapStartTime={minimapBounds.start}
|
||||||
showMinimap
|
minimapEndTime={minimapBounds.end}
|
||||||
minimapStartTime={minimapBounds.start}
|
events={reviewItems.all}
|
||||||
minimapEndTime={minimapBounds.end}
|
severityType={severity}
|
||||||
events={reviewItems.all}
|
contentRef={contentRef}
|
||||||
severityType={severity}
|
/>
|
||||||
contentRef={contentRef}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user