Files
frigate/web/src/components/preview/ScrubbablePreview.tsx
T
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Josh Hawkins
ee2ae1a957
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
Bump the radix group in /web with 22 updates (#24702)
* Bump the radix group in /web with 22 updates

Bumps the radix group in /web with 22 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.24` | `1.2.1` |
| [@radix-ui/react-aspect-ratio](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/aspect-ratio) | `1.1.16` | `1.2.0` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.12` | `1.4.1` |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.21` | `1.2.1` |
| [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.8` | `2.4.1` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.2.0` | `1.3.1` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.25` | `2.2.1` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.24` | `1.2.1` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.16` | `2.1.17` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.2.0` | `1.3.1` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.17` | `1.2.1` |
| [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radio-group) | `1.4.8` | `1.5.1` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.3.0` | `1.4.0` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.8` | `2.4.1` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.16` | `1.2.0` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.5.0` | `1.6.1` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.4.0` | `1.5.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.8` | `1.4.1` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.22` | `1.2.1` |
| [@radix-ui/react-toggle](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle) | `1.1.19` | `1.2.1` |
| [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) | `1.1.20` | `1.2.1` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.3.0` | `1.4.1` |


Updates `@radix-ui/react-alert-dialog` from 1.1.24 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-aspect-ratio` from 1.1.16 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/aspect-ratio/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/aspect-ratio)

Updates `@radix-ui/react-checkbox` from 1.3.12 to 1.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-collapsible` from 1.1.21 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-context-menu` from 2.3.8 to 2.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/context-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/context-menu)

Updates `@radix-ui/react-dialog` from 1.2.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.25 to 2.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-hover-card` from 1.1.24 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.16 to 2.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.2.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-progress` from 1.1.17 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress)

Updates `@radix-ui/react-radio-group` from 1.4.8 to 1.5.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radio-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radio-group)

Updates `@radix-ui/react-scroll-area` from 1.3.0 to 1.4.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.8 to 2.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.16 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slider` from 1.5.0 to 1.6.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-slot` from 1.4.0 to 1.5.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.8 to 1.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.22 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toggle` from 1.1.19 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle)

Updates `@radix-ui/react-toggle-group` from 1.1.20 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

Updates `@radix-ui/react-tooltip` from 1.3.0 to 1.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-aspect-ratio"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-context-menu"
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-radio-group"
  dependency-version: 1.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-toggle"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix radix bump type error and drop popper override

Slider 1.6 types its root as a span, so the progress hover handlers now take HTMLElement. The react-popper 1.2.8 override pinned an older popper that nested its own copies of primitive, context and the use-* hooks; without it every radix package resolves to a single copy, and compose-refs stays on the patched 1.1.2 through its own override.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-10-18 12:50:05 -05:00

486 lines
12 KiB
TypeScript

import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useApiHost } from "@/api";
import useSWR from "swr";
import { isFirefox, isMobile, isSafari } from "react-device-detect";
import { TimelineScrubMode, TimeRange } from "@/types/timeline";
import { NoThumbSlider } from "../ui/slider";
import { PREVIEW_FPS, PREVIEW_PADDING, Preview } from "@/types/preview";
import { baseUrl } from "@/api/baseUrl";
type VideoPreviewProps = {
relevantPreview: Preview;
startTime: number;
endTime?: number;
showProgress?: boolean;
loop?: boolean;
setReviewed: () => void;
setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void;
windowVisible: boolean;
};
export function VideoPreview({
relevantPreview,
startTime,
endTime,
showProgress = true,
loop = false,
setReviewed,
setIgnoreClick,
isPlayingBack,
onTimeUpdate,
windowVisible,
}: VideoPreviewProps) {
const playerRef = useRef<HTMLVideoElement | null>(null);
const sliderRef = useRef<HTMLDivElement | null>(null);
// keep track of playback state
const [progress, setProgress] = useState(0);
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
const playerStartTime = useMemo(() => {
if (!relevantPreview) {
return 0;
}
// start with a bit of padding
return Math.max(0, startTime - relevantPreview.start - PREVIEW_PADDING);
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const playerDuration = useMemo(
() => (endTime ?? relevantPreview.end) - startTime + PREVIEW_PADDING,
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const [lastPercent, setLastPercent] = useState(0.0);
// initialize player correctly
useEffect(() => {
if (!playerRef.current) {
return;
}
if (isSafari || (isFirefox && isMobile)) {
playerRef.current.pause();
setPlaybackMode("compat");
} else {
playerRef.current.currentTime = playerStartTime;
playerRef.current.playbackRate = PREVIEW_FPS;
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerRef]);
// time progress update
const onProgress = useCallback(() => {
if (!windowVisible) {
return;
}
if (onTimeUpdate) {
onTimeUpdate(
relevantPreview.start + (playerRef.current?.currentTime || 0),
);
}
const playerProgress =
(playerRef.current?.currentTime || 0) - playerStartTime;
// end with a bit of padding
const playerPercent = (playerProgress / playerDuration) * 100;
if (setReviewed && lastPercent < 50 && playerPercent > 50) {
setReviewed();
}
setLastPercent(playerPercent);
if (playerPercent > 100) {
setReviewed();
if (loop && playerRef.current) {
if (playbackMode != "auto") {
setPlaybackMode("auto");
setTimeout(() => setPlaybackMode("compat"), 100);
}
playerRef.current.currentTime = playerStartTime;
return;
}
if (isMobile) {
isPlayingBack(false);
if (onTimeUpdate) {
onTimeUpdate(undefined);
}
} else {
playerRef.current?.pause();
}
setPlaybackMode("auto");
setProgress(100.0);
} else {
setProgress(playerPercent);
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setProgress, lastPercent, windowVisible]);
// manual playback
// safari is incapable of playing at a speed > 2x
// so manual seeking is required on iOS
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
useEffect(() => {
if (playbackMode != "compat" || !playerRef.current) {
return;
}
let counter = 0;
const intervalId: NodeJS.Timeout = setInterval(() => {
if (playerRef.current) {
playerRef.current.currentTime = playerStartTime + counter;
counter += 1;
}
}, 1000 / PREVIEW_FPS);
return () => clearInterval(intervalId);
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playbackMode, playerRef]);
// user interaction
useEffect(() => {
setIgnoreClick(playbackMode != "auto" && playbackMode != "compat");
}, [playbackMode, setIgnoreClick]);
const onManualSeek = useCallback(
(values: number[]) => {
const value = values[0];
if (!playerRef.current) {
return;
}
if (playerRef.current.paused == false) {
playerRef.current.pause();
}
if (setReviewed) {
setReviewed();
}
setProgress(value);
playerRef.current.currentTime =
playerStartTime + (value / 100.0) * playerDuration;
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[playerDuration, playerRef, playerStartTime, setIgnoreClick],
);
const onStopManualSeek = useCallback(() => {
setTimeout(() => {
setHoverTimeout(undefined);
if (isSafari || (isFirefox && isMobile)) {
setPlaybackMode("compat");
} else {
setPlaybackMode("auto");
playerRef.current?.play();
}
}, 500);
}, [playerRef]);
const onProgressHover = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
if (!sliderRef.current) {
return;
}
const rect = sliderRef.current.getBoundingClientRect();
const positionX = event.clientX - rect.left;
const width = sliderRef.current.clientWidth;
onManualSeek([Math.round((positionX / width) * 100)]);
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
},
[sliderRef, hoverTimeout, onManualSeek],
);
return (
<div className="relative aspect-video size-full bg-black">
<video
ref={playerRef}
className="pointer-events-none aspect-video size-full bg-black"
autoPlay
playsInline
preload="auto"
muted
onTimeUpdate={onProgress}
>
<source
src={`${baseUrl}${relevantPreview.src.substring(1)}`}
type={relevantPreview.type}
/>
</video>
{showProgress && (
<NoThumbSlider
ref={sliderRef}
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode == "hover" || playbackMode == "drag" ? "h-4" : "h-2"}`}
value={[progress]}
onValueChange={(event) => {
setPlaybackMode("drag");
onManualSeek(event);
}}
onValueCommit={onStopManualSeek}
min={0}
step={1}
max={100}
onMouseMove={
isMobile
? undefined
: (event) => {
if (playbackMode != "drag") {
setPlaybackMode("hover");
onProgressHover(event);
}
}
}
onMouseLeave={
isMobile
? undefined
: () => {
if (!sliderRef.current) {
return;
}
setHoverTimeout(setTimeout(() => onStopManualSeek(), 500));
}
}
/>
)}
</div>
);
}
const MIN_LOAD_TIMEOUT_MS = 200;
type InProgressPreviewProps = {
camera: string;
startTime: number;
endTime?: number;
timeRange: TimeRange;
showProgress?: boolean;
loop?: boolean;
defaultImageUrl?: string;
setReviewed: () => void;
setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void;
windowVisible: boolean;
};
export function InProgressPreview({
camera,
startTime,
endTime,
timeRange,
showProgress = true,
loop = false,
defaultImageUrl,
setReviewed,
setIgnoreClick,
isPlayingBack,
onTimeUpdate,
windowVisible,
}: InProgressPreviewProps) {
const apiHost = useApiHost();
const sliderRef = useRef<HTMLDivElement | null>(null);
const { data: previewFrames } = useSWR<string[]>(
`preview/${camera}/start/${Math.floor(startTime) - PREVIEW_PADDING}/end/${Math.ceil(
endTime ?? timeRange.before,
)}/frames`,
{ revalidateOnFocus: false },
);
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
const [key, setKey] = useState(0);
const handleLoad = useCallback(() => {
if (!previewFrames || !windowVisible) {
return;
}
if (onTimeUpdate) {
onTimeUpdate(startTime - PREVIEW_PADDING + key);
}
if (playbackMode != "auto") {
return;
}
if (key == previewFrames.length - 1) {
setReviewed();
if (loop) {
setKey(0);
return;
}
if (isMobile) {
isPlayingBack(false);
if (onTimeUpdate) {
onTimeUpdate(undefined);
}
}
return;
}
setTimeout(() => {
if (setReviewed && key == Math.floor(previewFrames.length / 2)) {
setReviewed();
}
if (previewFrames[key + 1]) {
setKey(key + 1);
}
}, MIN_LOAD_TIMEOUT_MS);
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, playbackMode, previewFrames]);
// user interaction
useEffect(() => {
setIgnoreClick(playbackMode != "auto");
}, [playbackMode, setIgnoreClick]);
const onManualSeek = useCallback(
(values: number[]) => {
const value = values[0];
setReviewed();
setKey(value);
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[setIgnoreClick, setKey],
);
const onStopManualSeek = useCallback(
(values: number[]) => {
const value = values[0];
setTimeout(() => {
setPlaybackMode("auto");
setKey(value - 1);
}, 500);
},
[setPlaybackMode],
);
const onProgressHover = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
if (!sliderRef.current || !previewFrames) {
return;
}
const rect = sliderRef.current.getBoundingClientRect();
const positionX = event.clientX - rect.left;
const width = sliderRef.current.clientWidth;
const progress = [Math.round((positionX / width) * previewFrames.length)];
onManualSeek(progress);
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
},
[sliderRef, hoverTimeout, previewFrames, onManualSeek],
);
if (!previewFrames || previewFrames.length == 0) {
return (
<img
className="size-full"
src={defaultImageUrl} //{`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
/>
);
}
return (
<div className="relative flex size-full items-center bg-black">
<img
className="pointer-events-none size-full object-contain"
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
onLoad={handleLoad}
/>
{showProgress && (
<NoThumbSlider
ref={sliderRef}
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode != "auto" ? "h-4" : "h-2"}`}
value={[key]}
onValueChange={(event) => {
setPlaybackMode("drag");
onManualSeek(event);
}}
onValueCommit={onStopManualSeek}
min={0}
step={1}
max={previewFrames.length - 1}
onMouseMove={
isMobile
? undefined
: (event) => {
if (playbackMode != "drag") {
setPlaybackMode("hover");
onProgressHover(event);
}
}
}
onMouseLeave={
isMobile
? undefined
: (event) => {
if (!sliderRef.current || !previewFrames) {
return;
}
const rect = sliderRef.current.getBoundingClientRect();
const positionX = event.clientX - rect.left;
const width = sliderRef.current.clientWidth;
const progress = [
Math.round((positionX / width) * previewFrames.length),
];
setHoverTimeout(
setTimeout(() => onStopManualSeek(progress), 500),
);
}
}
/>
)}
</div>
);
}