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:
@@ -7,12 +7,13 @@ import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { useControlsVisibility } from "./plexPlayerHooks";
|
||||
|
||||
const setup = () => {
|
||||
const video = { paused: false } as HTMLVideoElement;
|
||||
const setup = (menuOpen = false) => {
|
||||
const video = document.createElement("video");
|
||||
Object.defineProperty(video, "paused", { configurable: true, value: false });
|
||||
return renderHook(() =>
|
||||
useControlsVisibility({
|
||||
barAutoHideRef: { current: true },
|
||||
menuOpenRef: { current: false },
|
||||
menuOpenRef: { current: menuOpen },
|
||||
videoRef: { current: video },
|
||||
}),
|
||||
);
|
||||
@@ -44,4 +45,19 @@ describe("useControlsVisibility", () => {
|
||||
act(() => vi.advanceTimersByTime(100));
|
||||
expect(result.current.uiVisible).toBe(false);
|
||||
});
|
||||
|
||||
// C3: on touch nothing moves a mouse, so a tap toggles the bar and playback arms the timer.
|
||||
it("toggle hides a shown bar at once and shows a hidden one", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle());
|
||||
expect(result.current.uiVisible).toBe(false);
|
||||
act(() => result.current.toggle());
|
||||
expect(result.current.uiVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("a tap does not hide the bar under an open menu", () => {
|
||||
const { result } = setup(true);
|
||||
act(() => result.current.toggle());
|
||||
expect(result.current.uiVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("the Plex player's layers", () => {
|
||||
const popovers = classes(
|
||||
/\bbottom-full\b.*\bbg-media-chrome\b|\bbg-media-chrome\b.*\bbottom-full\b/,
|
||||
);
|
||||
expect(popovers).toHaveLength(2);
|
||||
expect(popovers).toHaveLength(3); // tracks, the ⋯ menu (C5), the settings panel
|
||||
for (const c of popovers) expect(c).toMatch(/\bz-menu\b/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user