/** * Desktop UI overlay plugin for the video player. * * File map (desktop-ui/ folder): * * index.ts — DesktopUiPlugin class: static metadata, field * declarations, lifecycle overrides (use/disable/ * enable/dispose), public surface (overlay()), and * composeMixins() call. * internals.ts — DesktopUiInternals interface: the shared * `this`-context for every mixin method. * feedbackMethods.ts — wireFeedback / showMessage / hideMessage. * shortcutsMethods.ts — toggleShortcuts / showShortcuts / hideShortcuts. * activityMethods.ts — bumpActivity / maybeHide / dismissOverlay. * menuMethods.ts — Menu open/close/repaint/keyboard-nav. * iconStateMethods.ts — apply* button-icon / aria helpers. * transportStateMethods.ts — Time, duration, playing state, capability gating. * chapterMethods.ts — Chapter-marker DOM, progress/buffer/hover updates. * spriteMethods.ts — Sprite VTT loading + scrub-preview painting. * domMethods.ts — buildDom / wireTooltips / wireSliderBar / wireEvents. * activity.ts — setActivity / bumpActivity / maybeHide / dismissOverlay. * dom.ts — buildCenter / buildBottomBar / buildShortcutsOverlay. * responsive.ts — wireResponsive / wireOrientation / wireNoHover / * wireVolumeSlider / applyAllVisibilityRules. * tooltips.ts — addTooltip / clampTooltip / wireTooltips. * topBar.ts — Top-bar DOM + title/show-info update + back/cast/close buttons. * progressBar.ts — Slider-bar DOM, chapter-marker rendering, formatSeconds. * buttonState.ts — apply* pure DOM-mutation free functions. * menus.ts — Menu-frame DOM + all sub-pane renderers. * menuControl.ts — Menu open/close orchestration, keyboard nav, repaints. * buttons.ts — Fluent UI icon SVG path data table. * icons.ts — svgFromIcon() renderer on top of buttons.ts. * sprite.ts — Sprite VTT parser + thumbnail lookup. * chapters.ts — findChapterTitle / nextChapter / previousChapter. * * Mixin composition (how the class is built): * * The class body holds only: * - static metadata (id, version, description, translations) * - private field declarations (state owned by the plugin) * - lifecycle overrides that call `super` (use/disable/enable/dispose) * - the public `overlay()` surface * - `declare` signatures for every mixin method * * `composeMixins(DesktopUiPlugin.prototype, ...)` at the bottom stamps the * method bodies onto the prototype from the `*Methods` objects. * This mirrors exactly how NMVideoPlayer is built from playerCoreMethods. * * UX rule — menu vs. cycle: * Pointer-input buttons (control bar) open menus for multi-state features. * The cycle action (cycleAspectRatio, etc.) is for remote-control and key-bind * contexts where the user cannot pick from a list. Quality, subtitles, audio, * speed, and aspect-ratio are all menu-driven on click. Theater / PiP / * Fullscreen are binary toggles — direct action on click is correct for those. * * DOM tree: * * overlay * ├─ top-bar > title (topBar.ts) * ├─ center > spinner + center-btn * ├─ bottom-bar * │ ├─ bottom-bar-shadow * │ ├─ top-row (progressBar.ts) * │ │ └─ slider-bar * │ │ ├─ slider-buffer * │ │ ├─ slider-hover * │ │ ├─ slider-progress * │ │ ├─ chapter-progress × N * │ │ ├─ slider-nipple * │ │ └─ slider-pop * │ └─ bottom-row * │ ├─ transport buttons (buttonState.ts for icon state) * │ ├─ volume-container * │ ├─ current-time + remaining-time * │ └─ feature buttons * └─ menu-frame-dialog (menus.ts) * * Segmented-buffer rendering: when the item has chapters, sliderBuffer is * hidden and each chapter-marker carries its own buffer div. See progressBar.ts * for the scaleX fill math. The 2 px gap from `calc(width% - 2px)` aligns * segments with chapter dividers automatically. */ import type { Translations } from '@nomercy-entertainment/nomercy-player-core'; import type { IVideoPlayer, VideoPlaylistItem } from '../../index.js'; import type { SettingsToggleItem, SubMenuId, SubtitleMenuAction } from './helpers/menus.js'; import { Plugin } from '@nomercy-entertainment/nomercy-player-core'; /** * Per-button visibility overrides for the desktop UI control bar. * * Default-ON buttons (omit or set `true` to show): play, mute, volume, * fullscreen, settings, chapterPrev, chapterNext. * Chapter buttons are hidden automatically when the current item has no chapters * (content gating via `data-content-hidden`). * * Default-OFF buttons (set `true` to enable): theater, pip, speed, quality, * subtitles, audio, playlist, seekBack, seekForward, aspectRatio. * `seekBack` / `seekForward` default to false because ±10 s seek is available * on touch zones (double-tap) and keyboard (ArrowLeft/Right). Chapter buttons * are the unique value in the control bar. * * Navigation (always-on when queue has multiple items): next, previous. * * `cast` is the one entry here that isn't a control-bar button — it renders in * the TOP bar, next to back/close. Default-OFF; set * `true` to show it. Clicking it only emits the `cast` player event, the same * pattern the back button uses for `back` — the player never opens a device * picker itself, that's entirely the consumer's job. */ export interface DesktopUiButtonOptions { play?: boolean; mute?: boolean; volume?: boolean; fullscreen?: boolean; settings?: boolean; next?: boolean; previous?: boolean; theater?: boolean; pip?: boolean; speed?: boolean; quality?: boolean; subtitles?: boolean; audio?: boolean; playlist?: boolean; chapterPrev?: boolean; chapterNext?: boolean; seekBack?: boolean; seekForward?: boolean; aspectRatio?: boolean; /** Top-bar cast button, next to back/close. Default off. See interface doc. */ cast?: boolean; } /** * Priority order for responsive button removal. When the container narrows, * buttons at the END of the array are hidden first. The default order puts * the most essential buttons first so they survive longest. * * Only include buttons that are enabled via `buttons`. Buttons not in the list * keep whatever visibility the content rules gave them. */ export type ButtonPriorityList = ReadonlyArray; /** * A single responsive breakpoint. Below `maxWidth` (container pixels), only * buttons up to `hideAfterRank` in the priority list are shown. Rank 0 means * only the first button in the priority list survives; `Infinity` means * show all buttons. * * @example * // Hide everything past the 4th-priority button below 480 px: * { name: 'sm', maxWidth: 480, hideAfterRank: 3 } */ export interface Breakpoint { /** Human-readable name, also set as a `data-breakpoint` attribute on the container. */ name: string; /** Container width (px) at which this breakpoint activates. Use `Infinity` for the largest tier. */ maxWidth: number; /** Buttons with a priority rank strictly greater than this value are hidden at this breakpoint. */ hideAfterRank: number; } /** * Payload emitted on every breakpoint transition. * * Subscribe cross-plugin style: * ```ts * this.on(DesktopUiPlugin, 'layout:breakpoint', (data) => { * console.log(data.to, data.hiddenButtons); * }); * ``` */ export interface LayoutBreakpointPayload { /** Name of the breakpoint that was active before this resize. */ from: string; /** Name of the breakpoint now active. */ to: string; /** Button keys still visible at the new breakpoint. */ visibleButtons: ReadonlyArray; /** Button keys hidden by the new breakpoint (excludes always-hidden buttons). */ hiddenButtons: ReadonlyArray; } export interface DesktopUiOptions { hideTitle?: boolean; disableClickToPause?: boolean; inactivityMs?: number; imageBaseUrl?: string; /** Per-button opt-in / opt-out. Unset keys use the button's own default. */ buttons?: DesktopUiButtonOptions; /** * Visual order override. Buttons named here are re-anchored to the end * of the bar in the given sequence; unnamed buttons keep their natural * position. Independent of `buttonPriority` (responsive removal order). * * @example * buttonOrder: ['playlist', 'subtitles', 'audio', 'quality', 'pip', 'settings', 'fullscreen'] */ buttonOrder?: ReadonlyArray; /** * Consumer toggle rows appended to the settings main menu (e.g. an * app's auto-skip switch). Labels resolve at render time; `get`/`set` * bind each row to wherever the state lives. */ settingsItems?: ReadonlyArray; /** * Consumer action row(s) appended to the subtitles sub-menu (e.g. a * "Search subtitles online…" entry that opens the app's own dialog). * Shown whenever provided — including when the current item has zero * subtitle tracks, since that's exactly when an external-search action * matters most. */ subtitleMenuActions?: ReadonlyArray; /** * Consumer action row(s) appended to the MAIN settings menu, after any * `settingsItems` toggle rows — e.g. a "Cast to device…" entry that opens * the app's own device picker. Same row shape as `subtitleMenuActions`; * only the mount point differs. This is the sanctioned way to add an * app-owned action to the settings menu without the player taking any * opinion on what the action does — casting/device-switch is a consumer * concern, this only gives it a place to live. */ settingsMenuActions?: ReadonlyArray; /** * Priority order for responsive removal when the container is narrow. * Buttons at the end are removed first. Override to change the default order. * * Default order: play → mute → volume → fullscreen → settings → next → * previous → chapterPrev → chapterNext → seekBack → seekForward → * theater → pip → speed → quality → subtitles → audio → aspectRatio → playlist. */ buttonPriority?: ButtonPriorityList; /** * Buttons forced off in portrait regardless of available width, replacing the * default set entirely (pass `[]` to force nothing off and let width alone * decide). * * Portrait has room for roughly five or six controls, so something has to go; * which ones depends on the content. An episodic app wants `next` and * `chapterNext` to survive so a viewer can skip an intro one-handed, while a * single-video app would rather keep `quality`. * * Default: `chapterPrev`, `chapterNext`, `previous`, `next`, `subtitles`, * `audio`, `quality`, `playlist`. */ portraitHidden?: Array; /** * Full breakpoint progression. When provided, takes precedence over * `collapseStages`. Each entry says "below `maxWidth` px, hide buttons * whose priority rank exceeds `hideAfterRank`." * * Entries must be ordered from smallest `maxWidth` to largest. * The last entry should use `maxWidth: Infinity` to cover all wider sizes. * * @example * breakpoints: [ * { name: 'xs', maxWidth: 320, hideAfterRank: 1 }, * { name: 'sm', maxWidth: 480, hideAfterRank: 4 }, * { name: 'md', maxWidth: 720, hideAfterRank: 8 }, * { name: 'lg', maxWidth: 1024, hideAfterRank: 13 }, * { name: 'xl', maxWidth: Infinity, hideAfterRank: Infinity }, * ] */ breakpoints?: Breakpoint[]; /** * Shorthand alternative to `breakpoints`. Provide an array of `hideAfterRank` * values for the sm / md / lg tiers (xs is always rank 1, xl always shows all). * Ignored when `breakpoints` is provided. * * @example * // Hide after rank 2 at sm, rank 4 at md, rank 6 at lg: * collapseStages: [2, 4, 6] */ collapseStages?: [number, number, number]; /** * Volume slider orientation. * - `'horizontal'` — inline slider that expands on hover (default). * - `'vertical'` — popup slider above the mute button, toggle on click. * - `'auto'` — vertical when the player width is ≤ 520 px, else horizontal. */ volumeSlider?: 'horizontal' | 'vertical' | 'auto'; } /** Events emitted by {@link DesktopUiPlugin} under the `plugin:desktop-ui:` namespace. */ export interface DesktopUiEvents { 'shortcuts-toggle': undefined; 'layout:breakpoint': LayoutBreakpointPayload; 'opts:changed': DesktopUiOptions; } export declare class DesktopUiPlugin extends Plugin, DesktopUiOptions, DesktopUiEvents> { static readonly id: string; static readonly version: string; static readonly description: string; static readonly moduleUrl: string; static readonly translations: Translations; private overlayRoot; private topBarRefs; private centerWrap; private centerBtn; /** Center toast / status line — `display-message` renderer + loading/buffering/error feedback. Lives on the container so it survives overlay auto-hide. */ private messageEl; private messageTimer; /** `true` while the current message is playback feedback (loading/buffering/error) rather than a consumer toast — feedback clears automatically when playback recovers. */ private messageIsFeedback; private bottomBar; private sliderRefs; private chapterRefs; /** Sprite preview thumbnails for the current playlist item. */ private spriteSet; private spriteLoadId; private spriteObjectUrl; private isMouseDown; private isScrubbing; private _showRemaining; private playBtn; private prevBtn; private nextBtn; private rewindBtn; private forwardBtn; private chapBackBtn; private chapFwdBtn; private volBtn; private volSlider; /** Vertical volume slider popup. Null until `buildDom` creates it. */ private volSliderVertical; /** Mute toggle inside the vertical volume popup. Null until `buildDom` creates it. */ private volPopupMuteBtn; private currentTimeEl; private remainingTimeEl; private aspectRatioBtn; private speedBtn; private qualityBtn; private subsBtn; private audioBtn; private theaterBtn; /** Theater's config-level visibility, captured at build — state hiding (fullscreen/PiP) composes on top, never overrides an opt-out. */ private theaterConfigHidden; private fsActive; private pipActive; private pipBtn; private playlistBtn; private settingsBtn; private fsBtn; private menus; private _menuControlState; private _menuControlRefs; private shortcutsOverlay; private _shortcutsVisible; private _activityState; private _responsiveState; private cachedDuration; private _lastMouseX; private _lastMouseY; private _tooltipHoverToken; /** Initializes all mixin-owned state fields to their default values. Called at the top of `use()` before any mixin or DOM method runs. */ private initState; use(): void; /** * Disabling hides the overlay outright, not just its handlers. A peer that * owns the screen (e.g. a disc-menu interpreter painting the disc's own * chrome) disables this plugin to take over; leaving the rendered control * bar on top would double up the chrome. The inactivity timer is cancelled * and one final `activity:false` is emitted so nothing re-shows it while off. */ disable(reason?: string): void; /** Re-enabling restores the overlay and re-arms the auto-hide cycle. */ enable(): void; dispose(): void; /** * The overlay root — the auto-hiding chrome layer this plugin owns. * Other plugins mount their UI here (via * `player.getPlugin(DesktopUiPlugin)?.overlay()`) so their elements * inherit the overlay's show/hide lifecycle instead of floating over a * hidden chrome. Returns `null` before `use()` has built the DOM — a * field initializer would capture `undefined` forever, hence the method. */ overlay(): HTMLElement | null; /** * Pin the chrome visible while an external overlay of your own is open — a * device picker or cast panel that lives outside this plugin but is anchored * to its top/bottom bars, and so must not let them auto-hide underneath it. * Shows the chrome now and holds it until every hold is released. Balance * each call with exactly one `releaseChrome()`. * * This plugin's own menus don't need it — they already pin via `menuOpen`. */ holdChrome(): void; /** * Release one `holdChrome()`. The auto-hide countdown resumes only once the * last hold is gone. Extra releases are floored at zero rather than going * negative, so a double-release can't wedge the chrome permanently hidden. */ releaseChrome(): void; wireFeedback: () => void; showMessage: (text: string, ms?: number, isFeedback?: boolean) => void; hideMessage: () => void; toggleShortcuts: () => void; showShortcuts: () => void; hideShortcuts: () => void; bumpActivity: () => void; maybeHide: () => void; dismissOverlay: () => void; openMainMenu: () => void; openSubMenu: (id: SubMenuId) => void; wireMenuKeyboardNav: () => void; closeAllMenus: () => void; syncActiveIndexes: () => void; repaintSubsIfOpen: () => void; repaintAudioIfOpen: () => void; repaintQualityIfOpen: () => void; repaintSpeedIfOpen: () => void; repaintPlaylistIfOpen: () => void; repaintAspectRatioIfOpen: () => void; applyVolume: (level: number) => void; applyMuted: (muted: boolean) => void; applyMutedIcon: () => void; applyPopupMuteIcon: (muted: boolean) => void; applyRate: () => void; applyAudioIcon: () => void; applyQualityIcon: () => void; playingQualityLabel: () => string | undefined; resolvePlayingQualityIdx: () => number | null; applyFullscreen: () => void; applyTheaterIcon: (active: boolean) => void; applySubsIcon: () => void; applyMenuSubsIcon: () => void; applyPipIcon: (active: boolean) => void; applyAspectRatioIcon: () => void; setPlayingState: (playing: boolean) => void; handleCurrentChange: (item: VideoPlaylistItem | undefined | null) => void; applyTime: (seconds: number) => void; applyDuration: (dur: number) => void; _formatRemaining: (cur: number, dur: number) => string; applyStateVisibility: () => void; setContentHidden: (btn: HTMLButtonElement, hidden: boolean) => void; refreshCapabilityVisibility: () => void; refreshTransportEnablement: () => void; setDisabled: (btn: HTMLButtonElement, disabled: boolean) => void; safeCurrentIndex: () => number; safeQueueLength: () => number; resolveDuration: () => number; refreshChaptersAndDuration: () => void; renderChapterMarkers: () => void; updateChapterProgress: (pct: number) => void; updateChapterBuffer: (pct: number) => void; updateChapterHover: (pct: number) => void; findChapterTitle: (time: number) => string | undefined; previousChapter: () => void; nextChapter: () => void; getScrubTime: (event: Event) => { scrubTime: number; scrubTimePlayer: number; }; clampPopOffset: (pct: number) => number; paintSpriteAt: (time: number) => void; _resolveSpriteUrl: (item: VideoPlaylistItem | undefined | null) => string | undefined; _revokeSpriteObjectUrl: () => void; loadSpritesForItem: (item: VideoPlaylistItem | undefined | null) => Promise; buildDom: () => void; wireTooltips: () => void; applyInitialState: () => void; wireKeybindHint: () => void; wireSliderBar: () => void; wireEvents: () => void; } export declare const desktopUiPlugin: typeof DesktopUiPlugin;