mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-23 19:11:14 +01:00
Handle case where user stops scrubbing but remains hovering (#12794)
* Handle case where user stops scrubbing but remains hovering * Add type
This commit is contained in:
parent
54e1bd9eeb
commit
9c2974438d
@ -21,7 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|||||||
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
||||||
import useContextMenu from "@/hooks/use-contextmenu";
|
import useContextMenu from "@/hooks/use-contextmenu";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimelineScrubMode, TimeRange } from "@/types/timeline";
|
||||||
import { NoThumbSlider } from "../ui/slider";
|
import { NoThumbSlider } from "../ui/slider";
|
||||||
import { PREVIEW_FPS, PREVIEW_PADDING } from "@/types/preview";
|
import { PREVIEW_FPS, PREVIEW_PADDING } from "@/types/preview";
|
||||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||||
@ -414,7 +414,7 @@ export function VideoPreview({
|
|||||||
|
|
||||||
if (isSafari || (isFirefox && isMobile)) {
|
if (isSafari || (isFirefox && isMobile)) {
|
||||||
playerRef.current.pause();
|
playerRef.current.pause();
|
||||||
setManualPlayback(true);
|
setPlaybackMode("compat");
|
||||||
} else {
|
} else {
|
||||||
playerRef.current.currentTime = playerStartTime;
|
playerRef.current.currentTime = playerStartTime;
|
||||||
playerRef.current.playbackRate = PREVIEW_FPS;
|
playerRef.current.playbackRate = PREVIEW_FPS;
|
||||||
@ -453,9 +453,9 @@ export function VideoPreview({
|
|||||||
setReviewed();
|
setReviewed();
|
||||||
|
|
||||||
if (loop && playerRef.current) {
|
if (loop && playerRef.current) {
|
||||||
if (manualPlayback) {
|
if (playbackMode != "auto") {
|
||||||
setManualPlayback(false);
|
setPlaybackMode("auto");
|
||||||
setTimeout(() => setManualPlayback(true), 100);
|
setTimeout(() => setPlaybackMode("compat"), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.currentTime = playerStartTime;
|
playerRef.current.currentTime = playerStartTime;
|
||||||
@ -472,7 +472,7 @@ export function VideoPreview({
|
|||||||
playerRef.current?.pause();
|
playerRef.current?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
setManualPlayback(false);
|
setPlaybackMode("auto");
|
||||||
setProgress(100.0);
|
setProgress(100.0);
|
||||||
} else {
|
} else {
|
||||||
setProgress(playerPercent);
|
setProgress(playerPercent);
|
||||||
@ -486,9 +486,10 @@ export function VideoPreview({
|
|||||||
// safari is incapable of playing at a speed > 2x
|
// safari is incapable of playing at a speed > 2x
|
||||||
// so manual seeking is required on iOS
|
// so manual seeking is required on iOS
|
||||||
|
|
||||||
const [manualPlayback, setManualPlayback] = useState(false);
|
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!manualPlayback || !playerRef.current) {
|
if (playbackMode != "compat" || !playerRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,10 +504,14 @@ export function VideoPreview({
|
|||||||
|
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [manualPlayback, playerRef]);
|
}, [playbackMode, playerRef]);
|
||||||
|
|
||||||
// user interaction
|
// user interaction
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIgnoreClick(playbackMode != "auto" && playbackMode != "compat");
|
||||||
|
}, [playbackMode, setIgnoreClick]);
|
||||||
|
|
||||||
const onManualSeek = useCallback(
|
const onManualSeek = useCallback(
|
||||||
(values: number[]) => {
|
(values: number[]) => {
|
||||||
const value = values[0];
|
const value = values[0];
|
||||||
@ -515,14 +520,8 @@ export function VideoPreview({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualPlayback) {
|
|
||||||
setManualPlayback(false);
|
|
||||||
setIgnoreClick(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerRef.current.paused == false) {
|
if (playerRef.current.paused == false) {
|
||||||
playerRef.current.pause();
|
playerRef.current.pause();
|
||||||
setIgnoreClick(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setReviewed) {
|
if (setReviewed) {
|
||||||
@ -536,27 +535,21 @@ export function VideoPreview({
|
|||||||
|
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[playerDuration, playerRef, playerStartTime, setIgnoreClick],
|
||||||
manualPlayback,
|
|
||||||
playerDuration,
|
|
||||||
playerRef,
|
|
||||||
playerStartTime,
|
|
||||||
setIgnoreClick,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStopManualSeek = useCallback(() => {
|
const onStopManualSeek = useCallback(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIgnoreClick(false);
|
|
||||||
setHoverTimeout(undefined);
|
setHoverTimeout(undefined);
|
||||||
|
|
||||||
if (isSafari || (isFirefox && isMobile)) {
|
if (isSafari || (isFirefox && isMobile)) {
|
||||||
setManualPlayback(true);
|
setPlaybackMode("compat");
|
||||||
} else {
|
} else {
|
||||||
|
setPlaybackMode("auto");
|
||||||
playerRef.current?.play();
|
playerRef.current?.play();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, [playerRef, setIgnoreClick]);
|
}, [playerRef]);
|
||||||
|
|
||||||
const onProgressHover = useCallback(
|
const onProgressHover = useCallback(
|
||||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
@ -572,10 +565,8 @@ export function VideoPreview({
|
|||||||
if (hoverTimeout) {
|
if (hoverTimeout) {
|
||||||
clearTimeout(hoverTimeout);
|
clearTimeout(hoverTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHoverTimeout(setTimeout(() => onStopManualSeek(), 500));
|
|
||||||
},
|
},
|
||||||
[sliderRef, hoverTimeout, onManualSeek, onStopManualSeek, setHoverTimeout],
|
[sliderRef, hoverTimeout, onManualSeek],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -597,14 +588,37 @@ export function VideoPreview({
|
|||||||
{showProgress && (
|
{showProgress && (
|
||||||
<NoThumbSlider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${hoverTimeout != undefined ? "h-4" : "h-2"}`}
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode == "hover" || playbackMode == "drag" ? "h-4" : "h-2"}`}
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={(event) => {
|
||||||
|
setPlaybackMode("drag");
|
||||||
|
onManualSeek(event);
|
||||||
|
}}
|
||||||
onValueCommit={onStopManualSeek}
|
onValueCommit={onStopManualSeek}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
max={100}
|
max={100}
|
||||||
onMouseMove={isMobile ? undefined : onProgressHover}
|
onMouseMove={
|
||||||
|
isMobile
|
||||||
|
? undefined
|
||||||
|
: (event) => {
|
||||||
|
if (playbackMode != "drag") {
|
||||||
|
setPlaybackMode("hover");
|
||||||
|
onProgressHover(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseLeave={
|
||||||
|
isMobile
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
if (!sliderRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHoverTimeout(setTimeout(() => onStopManualSeek(), 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -642,7 +656,8 @@ export function InProgressPreview({
|
|||||||
}/frames`,
|
}/frames`,
|
||||||
{ revalidateOnFocus: false },
|
{ revalidateOnFocus: false },
|
||||||
);
|
);
|
||||||
const [manualFrame, setManualFrame] = useState(false);
|
|
||||||
|
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
|
||||||
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
|
|
||||||
@ -655,7 +670,7 @@ export function InProgressPreview({
|
|||||||
onTimeUpdate(review.start_time - PREVIEW_PADDING + key);
|
onTimeUpdate(review.start_time - PREVIEW_PADDING + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualFrame) {
|
if (playbackMode != "auto") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,19 +707,18 @@ export function InProgressPreview({
|
|||||||
|
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [key, manualFrame, previewFrames]);
|
}, [key, playbackMode, previewFrames]);
|
||||||
|
|
||||||
// user interaction
|
// user interaction
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIgnoreClick(playbackMode != "auto");
|
||||||
|
}, [playbackMode, setIgnoreClick]);
|
||||||
|
|
||||||
const onManualSeek = useCallback(
|
const onManualSeek = useCallback(
|
||||||
(values: number[]) => {
|
(values: number[]) => {
|
||||||
const value = values[0];
|
const value = values[0];
|
||||||
|
|
||||||
if (!manualFrame) {
|
|
||||||
setManualFrame(true);
|
|
||||||
setIgnoreClick(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!review.has_been_reviewed) {
|
if (!review.has_been_reviewed) {
|
||||||
setReviewed(review.id);
|
setReviewed(review.id);
|
||||||
}
|
}
|
||||||
@ -714,19 +728,18 @@ export function InProgressPreview({
|
|||||||
|
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[manualFrame, setIgnoreClick, setManualFrame, setKey],
|
[setIgnoreClick, setKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStopManualSeek = useCallback(
|
const onStopManualSeek = useCallback(
|
||||||
(values: number[]) => {
|
(values: number[]) => {
|
||||||
const value = values[0];
|
const value = values[0];
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIgnoreClick(false);
|
setPlaybackMode("auto");
|
||||||
setManualFrame(false);
|
|
||||||
setKey(value - 1);
|
setKey(value - 1);
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
[setManualFrame, setIgnoreClick],
|
[setPlaybackMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onProgressHover = useCallback(
|
const onProgressHover = useCallback(
|
||||||
@ -744,17 +757,8 @@ export function InProgressPreview({
|
|||||||
if (hoverTimeout) {
|
if (hoverTimeout) {
|
||||||
clearTimeout(hoverTimeout);
|
clearTimeout(hoverTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHoverTimeout(setTimeout(() => onStopManualSeek(progress), 500));
|
|
||||||
},
|
},
|
||||||
[
|
[sliderRef, hoverTimeout, previewFrames, onManualSeek],
|
||||||
sliderRef,
|
|
||||||
hoverTimeout,
|
|
||||||
previewFrames,
|
|
||||||
onManualSeek,
|
|
||||||
onStopManualSeek,
|
|
||||||
setHoverTimeout,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!previewFrames || previewFrames.length == 0) {
|
if (!previewFrames || previewFrames.length == 0) {
|
||||||
@ -776,14 +780,46 @@ export function InProgressPreview({
|
|||||||
{showProgress && (
|
{showProgress && (
|
||||||
<NoThumbSlider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${manualFrame ? "h-4" : "h-2"}`}
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode != "auto" ? "h-4" : "h-2"}`}
|
||||||
value={[key]}
|
value={[key]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={(event) => {
|
||||||
|
setPlaybackMode("drag");
|
||||||
|
onManualSeek(event);
|
||||||
|
}}
|
||||||
onValueCommit={onStopManualSeek}
|
onValueCommit={onStopManualSeek}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
max={previewFrames.length - 1}
|
max={previewFrames.length - 1}
|
||||||
onMouseMove={isMobile ? undefined : onProgressHover}
|
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>
|
</div>
|
||||||
|
@ -26,3 +26,5 @@ export type Timeline = {
|
|||||||
export type TimeRange = { before: number; after: number };
|
export type TimeRange = { before: number; after: number };
|
||||||
|
|
||||||
export type TimelineType = "timeline" | "events";
|
export type TimelineType = "timeline" | "events";
|
||||||
|
|
||||||
|
export type TimelineScrubMode = "auto" | "drag" | "hover" | "compat";
|
||||||
|
Loading…
Reference in New Issue
Block a user