/** * Touch / click zones plugin for the video player. * * Renders a transparent tap-zone overlay that sits at z-index 10 — above the * video element (z-index 0) but below the desktop UI controls overlay (z-index 20). * This ordering is intentional: touch zones intercept taps before they reach * the video element, but control buttons on the desktop-ui layer remain clickable. * * Layout: 3-column × 6-row CSS grid. * col 1 (left third) — seek backward zone * col 2 (centre) — play/pause + fullscreen zone * col 3 (right third) — seek forward zone * On mobile (detected via `ontouchstart` / `maxTouchPoints`) two extra zones are * added in col 2, rows 1–2 (volume up) and rows 5–6 (volume down), pushing the * play/pause zone to rows 3–5. * * Single-tap vs double-tap behaviour: * Left / right zones: single-tap shows/hides controls (toggle); double-tap seeks ±seekSeconds. * Centre zone: single-tap toggles play/pause; double-tap toggles fullscreen. * Volume zones (mobile only): single-tap shows/hides controls (toggle); double-tap adjusts volume. * * Integration: emits `player.emit('activity', { active })` to notify the * desktop-ui plugin that controls should be shown or hidden. All other calls * go through the typed player surface (rewind, forward, togglePlayback, etc.). * * Seek-feedback indicators * ──────────────────────── * Each seek zone hosts a floating indicator element (z-index 15 — above the * zones, below desktop-ui controls at z-index 20). The indicator is lazy-mounted * on the first double-tap of that side and remains in the DOM for the plugin's * lifetime. * * Layout: a circle pinned to the player edge (left/right), containing an SVG * chevron set and a text label showing the accumulated seek seconds for the * current burst (e.g. "−30s" after three rapid left-taps). * * Cumulative-count behaviour: * Each double-tap adds `seekSeconds` to a per-side accumulator and updates the * indicator text. A ~1 s collapse timer resets the accumulator when no further * taps arrive. Rapid successive taps update the same element — no stacking. * * Animation: CSS transitions on `opacity` and `transform`. The indicator gains an * `nm-seek-indicator--visible` class on show, loses it on hide. The transition * in/out is handled entirely by CSS (no JS animation frames). */ import type { Translations } from '@nomercy-entertainment/nomercy-player-core'; import type { NMVideoPlayer, VideoPlaylistItem } from '../../index.js'; import { Plugin } from '@nomercy-entertainment/nomercy-player-core'; export interface TouchZonesOptions { /** Milliseconds between taps that still counts as a double-tap. Default 300. */ doubleTapThreshold?: number; /** Seconds to seek on double-tap. Default 10. */ seekSeconds?: number; /** * When true, single-click/tap on the center zone never toggles playback. * Mirrors `disableClickToPause` on DesktopUiPlugin — both plugins ship their * own click handlers (desktop-ui on the video element, touch-zones on the * overlay zones), so the user needs the same switch on both. Default false. */ disableClickToPause?: boolean; } export declare class TouchZonesPlugin extends Plugin, TouchZonesOptions> { static readonly id: string; static readonly version: string; static readonly description: string; static readonly translations: Translations; private root; private _isMobile; /** * Snapshot of control visibility captured at touchstart (capture phase), * BEFORE desktop-ui's bubble-phase touchstart handler runs and potentially * mutates the DOM. `onSingle` reads this snapshot to decide hide-vs-nothing * based on what the user SAW when the tap started, not the post-wake state. * * Without this, a tap from hidden would: * 1. touchstart (bubble) → desktop-ui bumpActivity → .active added * 2. onSingle (300 ms later) → live DOM reads .active=true → hides * Net: show-then-hide flicker; tap appears to do nothing. * * Reset to null after each onSingle consumes it. Falls back to live DOM * for non-touch paths (mouse clicks on desktop have no touchstart). */ private _tapWasVisible; private leftIndicator; private rightIndicator; use(): void; dispose(): void; private detectMobile; /** * Returns the visibility of controls that should be used by the current * onSingle handler to decide whether to hide. * * On a touch path: returns the PRE-TAP snapshot captured at touchstart * (capture phase, before desktop-ui's bumpActivity may have added .active), * then clears the snapshot. Using the pre-tap value prevents the race where * "tap from hidden" shows controls (touchstart) then immediately hides them * (onSingle sees .active=true that was just added, emits activity:false). * * On a mouse/keyboard path: no touchstart fires, so the snapshot is null. * Falls back to the live `.active` class — correct for desktop where there * is no doubleTap deferred window that races against pointer input. */ private wasControlsVisibleAtTapStart; private makeBox; private doubleTap; /** * Drive the PLAYER's activity state to hidden. `activity` is a player-level * event (desktop-ui listens to toggle control visibility), not a * touch-zones-scoped one, so it goes on the player bus directly — the scoped * `this.emit()` would namespace it to `plugin:touch-zones:activity`, where * nothing listens. Used by the single-tap paths to re-hide controls that were * already visible at tap start. */ private dismissControls; private createSeekIndicator; private showSeekIndicator; private buildSeekBack; private buildSeekForward; private buildPlayback; private buildVolUp; private buildVolDown; } export declare const touchZonesPlugin: typeof TouchZonesPlugin;