mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Dynamically scale the slider height when hovering + other UI tweaks (#11042)
* Make no thumb slider height dynamic * Use existing switch component * Use existing switch component for general filter content * Show message when no reordings found for time * Don't show while scrubbing * Fix key error * Fix background color for controls on motion page
This commit is contained in:
parent
fe4fb645d3
commit
bfefed4d6e
@ -593,40 +593,26 @@ export function GeneralFilterContent({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="my-2.5 flex flex-col gap-2.5">
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<div className="flex justify-between items-center">
|
<FilterSwitch
|
||||||
<Label
|
label={item.replaceAll("_", " ")}
|
||||||
className="w-full mx-2 text-primary capitalize cursor-pointer"
|
isChecked={currentLabels?.includes(item) ?? false}
|
||||||
htmlFor={item}
|
onCheckedChange={(isChecked) => {
|
||||||
>
|
if (isChecked) {
|
||||||
{item.replaceAll("_", " ")}
|
const updatedLabels = currentLabels ? [...currentLabels] : [];
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
key={item}
|
|
||||||
className="ml-1"
|
|
||||||
id={item}
|
|
||||||
checked={currentLabels?.includes(item) ?? false}
|
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
const updatedLabels = currentLabels
|
|
||||||
? [...currentLabels]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
updatedLabels.push(item);
|
updatedLabels.push(item);
|
||||||
|
setCurrentLabels(updatedLabels);
|
||||||
|
} else {
|
||||||
|
const updatedLabels = currentLabels ? [...currentLabels] : [];
|
||||||
|
|
||||||
|
// can not deselect the last item
|
||||||
|
if (updatedLabels.length > 1) {
|
||||||
|
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
||||||
setCurrentLabels(updatedLabels);
|
setCurrentLabels(updatedLabels);
|
||||||
} else {
|
|
||||||
const updatedLabels = currentLabels
|
|
||||||
? [...currentLabels]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (updatedLabels.length > 1) {
|
|
||||||
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
|
||||||
setCurrentLabels(updatedLabels);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -520,6 +520,7 @@ export function VideoPreview({
|
|||||||
const onStopManualSeek = useCallback(() => {
|
const onStopManualSeek = useCallback(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIgnoreClick(false);
|
setIgnoreClick(false);
|
||||||
|
setHoverTimeout(undefined);
|
||||||
|
|
||||||
if (isSafari || (isFirefox && isMobile)) {
|
if (isSafari || (isFirefox && isMobile)) {
|
||||||
setManualPlayback(true);
|
setManualPlayback(true);
|
||||||
@ -565,7 +566,7 @@ export function VideoPreview({
|
|||||||
{showProgress && (
|
{showProgress && (
|
||||||
<NoThumbSlider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${hoverTimeout != undefined ? "h-4" : "h-2"}`}
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={onManualSeek}
|
||||||
onValueCommit={onStopManualSeek}
|
onValueCommit={onStopManualSeek}
|
||||||
@ -740,7 +741,7 @@ export function InProgressPreview({
|
|||||||
{showProgress && (
|
{showProgress && (
|
||||||
<NoThumbSlider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${manualFrame ? "h-4" : "h-2"}`}
|
||||||
value={[key]}
|
value={[key]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={onManualSeek}
|
||||||
onValueCommit={onStopManualSeek}
|
onValueCommit={onStopManualSeek}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Recording } from "@/types/record";
|
import { Recording } from "@/types/record";
|
||||||
import { DynamicPlayback } from "@/types/playback";
|
import { DynamicPlayback } from "@/types/playback";
|
||||||
import { PreviewController } from "../PreviewPlayer";
|
import { PreviewController } from "../PreviewPlayer";
|
||||||
import { Timeline } from "@/types/timeline";
|
import { TimeRange, Timeline } from "@/types/timeline";
|
||||||
|
|
||||||
type PlayerMode = "playback" | "scrubbing";
|
type PlayerMode = "playback" | "scrubbing";
|
||||||
|
|
||||||
@ -10,11 +10,13 @@ export class DynamicVideoController {
|
|||||||
public camera = "";
|
public camera = "";
|
||||||
private playerController: HTMLVideoElement;
|
private playerController: HTMLVideoElement;
|
||||||
private previewController: PreviewController;
|
private previewController: PreviewController;
|
||||||
|
private setNoRecording: (noRecs: boolean) => void;
|
||||||
private setFocusedItem: (timeline: Timeline) => void;
|
private setFocusedItem: (timeline: Timeline) => void;
|
||||||
private playerMode: PlayerMode = "playback";
|
private playerMode: PlayerMode = "playback";
|
||||||
|
|
||||||
// playback
|
// playback
|
||||||
private recordings: Recording[] = [];
|
private recordings: Recording[] = [];
|
||||||
|
private timeRange: TimeRange = { after: 0, before: 0 };
|
||||||
private annotationOffset: number;
|
private annotationOffset: number;
|
||||||
private timeToStart: number | undefined = undefined;
|
private timeToStart: number | undefined = undefined;
|
||||||
|
|
||||||
@ -24,6 +26,7 @@ export class DynamicVideoController {
|
|||||||
previewController: PreviewController,
|
previewController: PreviewController,
|
||||||
annotationOffset: number,
|
annotationOffset: number,
|
||||||
defaultMode: PlayerMode,
|
defaultMode: PlayerMode,
|
||||||
|
setNoRecording: (noRecs: boolean) => void,
|
||||||
setFocusedItem: (timeline: Timeline) => void,
|
setFocusedItem: (timeline: Timeline) => void,
|
||||||
) {
|
) {
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
@ -31,11 +34,13 @@ export class DynamicVideoController {
|
|||||||
this.previewController = previewController;
|
this.previewController = previewController;
|
||||||
this.annotationOffset = annotationOffset;
|
this.annotationOffset = annotationOffset;
|
||||||
this.playerMode = defaultMode;
|
this.playerMode = defaultMode;
|
||||||
|
this.setNoRecording = setNoRecording;
|
||||||
this.setFocusedItem = setFocusedItem;
|
this.setFocusedItem = setFocusedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
newPlayback(newPlayback: DynamicPlayback) {
|
newPlayback(newPlayback: DynamicPlayback) {
|
||||||
this.recordings = newPlayback.recordings;
|
this.recordings = newPlayback.recordings;
|
||||||
|
this.timeRange = newPlayback.timeRange;
|
||||||
|
|
||||||
if (this.timeToStart) {
|
if (this.timeToStart) {
|
||||||
this.seekToTimestamp(this.timeToStart);
|
this.seekToTimestamp(this.timeToStart);
|
||||||
@ -52,12 +57,17 @@ export class DynamicVideoController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
seekToTimestamp(time: number, play: boolean = false) {
|
seekToTimestamp(time: number, play: boolean = false) {
|
||||||
|
if (time < this.timeRange.after || time > this.timeRange.before) {
|
||||||
|
this.timeToStart = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.recordings.length == 0 ||
|
this.recordings.length == 0 ||
|
||||||
time < this.recordings[0].start_time ||
|
time < this.recordings[0].start_time ||
|
||||||
time > this.recordings[this.recordings.length - 1].end_time
|
time > this.recordings[this.recordings.length - 1].end_time
|
||||||
) {
|
) {
|
||||||
this.timeToStart = time;
|
this.setNoRecording(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +100,8 @@ export class DynamicVideoController {
|
|||||||
} else {
|
} else {
|
||||||
this.playerController.pause();
|
this.playerController.pause();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`seek time is 0`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ export default function DynamicVideoPlayer({
|
|||||||
const playerRef = useRef<HTMLVideoElement | null>(null);
|
const playerRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const [previewController, setPreviewController] =
|
const [previewController, setPreviewController] =
|
||||||
useState<PreviewController | null>(null);
|
useState<PreviewController | null>(null);
|
||||||
|
const [noRecording, setNoRecording] = useState(false);
|
||||||
const controller = useMemo(() => {
|
const controller = useMemo(() => {
|
||||||
if (!config || !playerRef.current || !previewController) {
|
if (!config || !playerRef.current || !previewController) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -56,6 +57,7 @@ export default function DynamicVideoPlayer({
|
|||||||
previewController,
|
previewController,
|
||||||
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
|
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
|
||||||
isScrubbing ? "scrubbing" : "playback",
|
isScrubbing ? "scrubbing" : "playback",
|
||||||
|
setNoRecording,
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
// we only want to fire once when players are ready
|
// we only want to fire once when players are ready
|
||||||
@ -92,9 +94,11 @@ export default function DynamicVideoPlayer({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (loadingTimeout) {
|
if (loadingTimeout) {
|
||||||
clearTimeout(loadingTimeout)
|
clearTimeout(loadingTimeout);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
// we only want trigger when scrubbing state changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [camera, isScrubbing]);
|
}, [camera, isScrubbing]);
|
||||||
|
|
||||||
const onPlayerLoaded = useCallback(() => {
|
const onPlayerLoaded = useCallback(() => {
|
||||||
@ -149,6 +153,7 @@ export default function DynamicVideoPlayer({
|
|||||||
|
|
||||||
controller.newPlayback({
|
controller.newPlayback({
|
||||||
recordings: recordings ?? [],
|
recordings: recordings ?? [],
|
||||||
|
timeRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
// we only want this to change when recordings update
|
// we only want this to change when recordings update
|
||||||
@ -175,6 +180,7 @@ export default function DynamicVideoPlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setNoRecording(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
@ -188,9 +194,14 @@ export default function DynamicVideoPlayer({
|
|||||||
setPreviewController(previewController);
|
setPreviewController(previewController);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && !noRecording && (
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
)}
|
)}
|
||||||
|
{!isScrubbing && noRecording && (
|
||||||
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
No recordings found for this time
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ const NoThumbSlider = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full">
|
<SliderPrimitive.Track className="relative h-full w-full grow overflow-hidden rounded-full">
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="block h-4 w-16 rounded-full bg-transparent -translate-y-[50%] ring-offset-transparent focus-visible:outline-none focus-visible:ring-transparent disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" />
|
<SliderPrimitive.Thumb className="block h-4 w-16 rounded-full bg-transparent -translate-y-[50%] ring-offset-transparent focus-visible:outline-none focus-visible:ring-transparent disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" />
|
||||||
|
@ -4,6 +4,7 @@ import { TimeRange } from "./timeline";
|
|||||||
|
|
||||||
export type DynamicPlayback = {
|
export type DynamicPlayback = {
|
||||||
recordings: Recording[];
|
recordings: Recording[];
|
||||||
|
timeRange: TimeRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreviewPlayback = {
|
export type PreviewPlayback = {
|
||||||
|
@ -591,7 +591,9 @@ function DetectionReview({
|
|||||||
})
|
})
|
||||||
: Array(itemsToReview)
|
: Array(itemsToReview)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map(() => <Skeleton className="size-full aspect-video" />)}
|
.map((_, idx) => (
|
||||||
|
<Skeleton key={idx} className="size-full aspect-video" />
|
||||||
|
))}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
(currentItems?.length ?? 0) > 0 &&
|
(currentItems?.length ?? 0) > 0 &&
|
||||||
(itemsToReview ?? 0) > 0 && (
|
(itemsToReview ?? 0) > 0 && (
|
||||||
@ -953,7 +955,7 @@ function MotionReview({
|
|||||||
|
|
||||||
{!scrubbing && (
|
{!scrubbing && (
|
||||||
<VideoControls
|
<VideoControls
|
||||||
className="absolute bottom-16 left-1/2 -translate-x-1/2"
|
className="absolute bottom-16 left-1/2 -translate-x-1/2 bg-secondary"
|
||||||
features={{
|
features={{
|
||||||
volume: false,
|
volume: false,
|
||||||
seek: true,
|
seek: true,
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
useSnapshotsState,
|
useSnapshotsState,
|
||||||
} from "@/api/ws";
|
} from "@/api/ws";
|
||||||
import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle";
|
import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle";
|
||||||
|
import FilterSwitch from "@/components/filter/FilterSwitch";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
import LivePlayer from "@/components/player/LivePlayer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||||
@ -15,8 +16,6 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
@ -623,67 +622,29 @@ function FrigateCameraFeatures({
|
|||||||
/>
|
/>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent className="px-2 py-4 flex flex-col gap-3 rounded-2xl">
|
<DrawerContent className="px-2 py-4 flex flex-col gap-3 rounded-2xl">
|
||||||
<div className="flex justify-between items-center gap-1">
|
<FilterSwitch
|
||||||
<Label
|
label="Object Detection"
|
||||||
className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
|
isChecked={detectState == "ON"}
|
||||||
htmlFor={"camera-detect"}
|
onCheckedChange={() => sendDetect(detectState == "ON" ? "OFF" : "ON")}
|
||||||
>
|
/>
|
||||||
Object Detection
|
<FilterSwitch
|
||||||
</Label>
|
label="Recording"
|
||||||
<Switch
|
isChecked={recordState == "ON"}
|
||||||
id={"camera-detect"}
|
onCheckedChange={() => sendRecord(recordState == "ON" ? "OFF" : "ON")}
|
||||||
checked={detectState == "ON"}
|
/>
|
||||||
onCheckedChange={() =>
|
<FilterSwitch
|
||||||
sendDetect(detectState == "ON" ? "OFF" : "ON")
|
label="Snapshots"
|
||||||
}
|
isChecked={snapshotState == "ON"}
|
||||||
/>
|
onCheckedChange={() =>
|
||||||
</div>
|
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
|
||||||
<div className="flex justify-between items-center gap-1">
|
}
|
||||||
<Label
|
/>
|
||||||
className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
|
|
||||||
htmlFor={"camera-record"}
|
|
||||||
>
|
|
||||||
Recording
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id={"camera-record"}
|
|
||||||
checked={recordState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendRecord(recordState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center gap-1">
|
|
||||||
<Label
|
|
||||||
className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
|
|
||||||
htmlFor={"camera-snapshot"}
|
|
||||||
>
|
|
||||||
Snapshots
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id={"camera-snapshot"}
|
|
||||||
checked={snapshotState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{audioDetectEnabled && (
|
{audioDetectEnabled && (
|
||||||
<div className="flex justify-between items-center gap-1">
|
<FilterSwitch
|
||||||
<Label
|
label="Audio Detection"
|
||||||
className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
|
isChecked={audioState == "ON"}
|
||||||
htmlFor={"camera-audio-detect"}
|
onCheckedChange={() => sendAudio(audioState == "ON" ? "OFF" : "ON")}
|
||||||
>
|
/>
|
||||||
Audio Detection
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id={"camera-audio-detect"}
|
|
||||||
checked={audioState == "ON"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
sendAudio(audioState == "ON" ? "OFF" : "ON")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
Loading…
Reference in New Issue
Block a user