Zum Inhalt

src/player/ControlsOverlay.ts

// src/player/ControlsOverlay.ts
import type { ControlsMode } from './types';

export interface ControlsHandlers {
  onPlayPause: () => void;
  onPrev: () => void;
  onNext: () => void;
  onFullscreen: () => void;
}

/** Plain-DOM control bar for the player. Lives inside the element's shadow
 *  root; styling is provided by the host's shadow stylesheet. */
export class ControlsOverlay {
  readonly root: HTMLDivElement;
  private readonly playBtn: HTMLButtonElement;
  private readonly prevBtn: HTMLButtonElement;
  private readonly nextBtn: HTMLButtonElement;

  constructor(doc: Document, mode: ControlsMode, hasPlaylist: boolean, h: ControlsHandlers) {
    const root = doc.createElement('div');
    root.className = `phys-controls phys-controls--${mode}`;

    this.prevBtn = this.button(doc, '‹', 'Previous preset', h.onPrev);
    this.playBtn = this.button(doc, '⏸', 'Play / pause', h.onPlayPause);
    this.nextBtn = this.button(doc, '›', 'Next preset', h.onNext);
    const fsBtn = this.button(doc, '⛶', 'Fullscreen', h.onFullscreen);
    // Only offer fullscreen where it's actually available. Inside an <iframe> embed this is false
    // unless the host page granted `allow="fullscreen"`, so the button self-hides instead of being a
    // dead control whose requestFullscreen() is silently rejected.
    fsBtn.hidden = !doc.fullscreenEnabled;

    this.prevBtn.hidden = !hasPlaylist;
    this.nextBtn.hidden = !hasPlaylist;

    root.append(this.prevBtn, this.playBtn, this.nextBtn, fsBtn);
    this.root = root;
  }

  private button(
    doc: Document,
    label: string,
    aria: string,
    onClick: () => void,
  ): HTMLButtonElement {
    const b = doc.createElement('button');
    b.type = 'button';
    b.textContent = label;
    b.setAttribute('aria-label', aria);
    b.addEventListener('click', (e) => {
      e.stopPropagation();
      onClick();
    });
    return b;
  }

  setPaused(paused: boolean): void {
    this.playBtn.textContent = paused ? '▶' : '⏸';
  }
}