Remake touch controls

This commit is contained in:
Zoe Roux 2025-07-25 22:56:50 +02:00
parent fc9695a2dc
commit 9343bb524c
No known key found for this signature in database
2 changed files with 117 additions and 125 deletions

View File

@ -150,131 +150,6 @@ export const Controls = ({
);
};
export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
const hover = useAtomValue(hoverAtom);
const setHover = useSetAtom(hoverReasonAtom);
const mouseCallback = useRef<NodeJS.Timeout | null>(null);
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({
count: 0,
});
const playerWidth = useRef<number | null>(null);
const isTouch = useIsTouch();
const show = useCallback(() => {
setHover((x) => ({ ...x, mouseMoved: true }));
if (mouseCallback.current) clearTimeout(mouseCallback.current);
mouseCallback.current = setTimeout(() => {
setHover((x) => ({ ...x, mouseMoved: false }));
}, 2500);
}, [setHover]);
// On mouse move
useEffect(() => {
if (Platform.OS !== "web") return;
const handler = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
show();
};
document.addEventListener("pointermove", handler);
return () => document.removeEventListener("pointermove", handler);
}, [show]);
// When the controls hide, remove focus so space can be used to play/pause instead of triggering the button
// It also serves to hide the tooltip.
useEffect(() => {
if (Platform.OS !== "web") return;
if (!hover && document.activeElement instanceof HTMLElement)
document.activeElement.blur();
}, [hover]);
const { css } = useYoshiki();
const duration = useAtomValue(durationAtom);
const setPlay = useSetAtom(playAtom);
const setProgress = useSetAtom(progressAtom);
const setFullscreen = useSetAtom(fullscreenAtom);
const onPress = (e: { pointerType: string; x: number }) => {
if (Platform.OS === "web" && e.pointerType === "mouse") {
setPlay((x) => !x);
return;
}
if (hover) setHover((x) => ({ ...x, mouseMoved: false }));
else show();
};
const onDoublePress = (e: { pointerType: string; x: number }) => {
if (Platform.OS === "web" && e.pointerType === "mouse") {
// Only reset touch count for the web, on mobile you can continue to seek by pressing again.
touch.current.count = 0;
setFullscreen((x) => !x);
return;
}
show();
if (!duration || !playerWidth.current) return;
if (e.x < playerWidth.current * 0.33) {
setProgress((x) => Math.max(x - 10, 0));
}
if (e.x > playerWidth.current * 0.66) {
setProgress((x) => Math.min(x + 10, duration));
}
};
const onAnyPress = (e: { pointerType: string; x: number }) => {
touch.current.count++;
if (touch.current.count >= 2) {
onDoublePress(e);
clearTimeout(touch.current.timeout);
} else {
onPress(e);
}
touch.current.timeout = setTimeout(() => {
touch.current.count = 0;
touch.current.timeout = undefined;
}, 400);
};
return (
<Pressable
tabIndex={-1}
onPointerLeave={(e) => {
if (e.nativeEvent.pointerType === "mouse")
setHover((x) => ({ ...x, mouseMoved: false }));
}}
onPress={(e) => {
e.preventDefault();
onAnyPress({
pointerType: isTouch ? "touch" : "mouse",
x: e.nativeEvent.locationX ?? e.nativeEvent.pageX,
});
}}
onLayout={(e) => {
playerWidth.current = e.nativeEvent.layout.width;
}}
{...css(
// @ts-expect-error Web only property (cursor: unset)
{
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
cursor: hover ? "unset" : "none",
},
props,
)}
>
{children}
</Pressable>
);
};
const VideoPoster = ({
poster,
alt,

View File

@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
type GestureResponderEvent,
Platform,
Pressable,
type PressableProps,
} from "react-native";
import type { VideoPlayer } from "react-native-video";
import { useYoshiki } from "yoshiki/native";
import { useIsTouch } from "~/primitives";
export const TouchControls = ({
player,
children,
...props
}: { player: VideoPlayer } & PressableProps) => {
const { css } = useYoshiki();
const isTouch = useIsTouch();
const [shouldShow, setShow] = useState(true);
const hideTimeout = useRef<NodeJS.Timeout | null>(null);
const show = useCallback((val: boolean = true) => {
setShow(val);
if (hideTimeout.current) clearTimeout(hideTimeout.current);
hideTimeout.current = setTimeout(() => {
hideTimeout.current = null;
setShow(false);
}, 2500);
}, []);
// TODO: handle mouse hover & seek
// On mouse move
useEffect(() => {
if (Platform.OS !== "web") return;
const handler = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
show();
};
document.addEventListener("pointermove", handler);
return () => document.removeEventListener("pointermove", handler);
}, [show]);
const playerWidth = useRef<number | null>(null);
return (
<DoublePressable
onPress={() => {
if (isTouch) {
show(!shouldShow);
return;
}
if (player.isPlaying) player.pause();
else player.play();
}}
onDoublePress={(e) => {
if (!isTouch) {
player.toggleFullscreen();
return;
}
show();
if (Number.isNaN(player.duration) || !playerWidth.current) return;
const x = e.nativeEvent.locationX ?? e.nativeEvent.pageX;
if (x < playerWidth.current * 0.33) player.seekBy(-10);
if (x > playerWidth.current * 0.66) player.seekBy(10);
// Do not reset press count, you can continue to seek by pressing again.
return true;
}}
onLayout={(e) => {
playerWidth.current = e.nativeEvent.layout.width;
}}
onPointerLeave={(e) => {
// instantly hide the controls when mouse leaves the view
if (e.nativeEvent.pointerType === "mouse") show(false);
}}
{...css({ cursor: "none" as any }, props)}
>
{shouldShow && children}
</DoublePressable>
);
};
const DoublePressable = ({
onPress,
onDoublePress,
...props
}: {
onDoublePress: (e: GestureResponderEvent) => boolean | undefined;
} & PressableProps) => {
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({
count: 0,
});
return (
<Pressable
onPress={(e) => {
e.preventDefault();
touch.current.count++;
if (touch.current.count >= 2) {
const keepCount = onDoublePress(e);
if (!keepCount) touch.current.count = 0;
clearTimeout(touch.current.timeout);
} else {
onPress?.(e);
}
touch.current.timeout = setTimeout(() => {
touch.current.count = 0;
touch.current.timeout = undefined;
}, 400);
}}
{...props}
/>
);
};