feat(design): players on touch and the C5 control set, restart on the card (E28 S3f2)

Plex player: a finger's tap on the picture only shows or hides the controls,
the hide timer starts with playback, a double-tap on a side third skips 10 s,
the seek bar is a 2.75rem touch-none hit area with its head shown on touch.
The row is play/pause, Back by position, Next only with a next item, mute,
time, tracks, a fixed ⋯ (settings, media info, stats, shortcuts, download)
and fullscreen; Stop, Restart and the ±10 s buttons are gone.

A started card shows the video card's Continue + Restart pair (one shared
MediaResumeButtons, beside the picture's button), and its menu lists the
restart; the player opens that item at 0 (fromStart). YouTube modal: the
description opens from an ⓘ, the queue arrows sit inside the screen.
This commit is contained in:
2026-09-27 02:12:54 +02:00
parent 17916b411a
commit 89e65ca331
33 changed files with 1100 additions and 282 deletions
+43 -6
View File
@@ -73,25 +73,50 @@ export function useFullscreen(wrapRef: RefObject<HTMLElement | null>): {
// `holdOnHover` is the pointer resting on the chrome: the bar stays while it is there (as on YouTube
// and Plex Web — E28 F22, it used to fade out from under a mouse parked on Next), and leaving it
// starts a fresh 3 s.
//
// On touch (C3) nothing moves a mouse, so the timer is also armed by PLAYBACK starting — before, the
// bar stayed up until the first tap however long the video played — and a tap on the picture only
// shows or hides the bar (`toggle`), never pauses. `show(false)` hides at once: a double-tap skip
// takes back the show/hide its first tap did.
export function useControlsVisibility(opts: {
barAutoHideRef: RefObject<boolean>;
menuOpenRef: RefObject<boolean>;
videoRef: RefObject<HTMLVideoElement | null>;
}): { uiVisible: boolean; wake: () => void; holdOnHover: (over: boolean) => void } {
}): {
uiVisible: boolean;
wake: () => void;
holdOnHover: (over: boolean) => void;
toggle: () => void;
show: (visible: boolean) => void;
} {
const { barAutoHideRef, menuOpenRef, videoRef } = opts;
const [uiVisible, setUiVisible] = useState(true);
const visibleRef = useRef(true);
const hideTimer = useRef<number | null>(null);
const hovered = useRef(false);
const setVisible = useCallback((v: boolean) => {
visibleRef.current = v;
setUiVisible(v);
}, []);
const wake = useCallback(() => {
setUiVisible(true);
setVisible(true);
if (hideTimer.current) window.clearTimeout(hideTimer.current);
if (!barAutoHideRef.current) return; // per-user setting: never auto-hide the control bar
hideTimer.current = window.setTimeout(() => {
// Keep the controls up while the audio/subtitle menu is open, else it would vanish mid-choice.
if (!videoRef.current?.paused && !menuOpenRef.current && !hovered.current)
setUiVisible(false);
if (!videoRef.current?.paused && !menuOpenRef.current && !hovered.current) setVisible(false);
}, 3000);
}, [barAutoHideRef, menuOpenRef, videoRef]);
}, [barAutoHideRef, menuOpenRef, videoRef, setVisible]);
const show = useCallback(
(visible: boolean) => {
if (visible) return wake();
if (hideTimer.current) window.clearTimeout(hideTimer.current);
// A tap on a picture with a menu open closes nothing and must not strand that menu unseen.
if (!menuOpenRef.current) setVisible(false);
},
[wake, menuOpenRef, setVisible],
);
const toggle = useCallback(() => show(!visibleRef.current), [show]);
const holdOnHover = useCallback(
(over: boolean) => {
hovered.current = over;
@@ -99,7 +124,19 @@ export function useControlsVisibility(opts: {
},
[wake],
);
return { uiVisible, wake, holdOnHover };
useEffect(() => {
const video = videoRef.current;
if (!video) return;
video.addEventListener("play", wake);
return () => video.removeEventListener("play", wake);
}, [videoRef, wake]);
useEffect(
() => () => {
if (hideTimer.current) window.clearTimeout(hideTimer.current);
},
[],
);
return { uiVisible, wake, holdOnHover, toggle, show };
}
// Periodic + on-unmount progress checkpoint. Reads live position/duration through refs (owned by the