import type { PlayerStore } from '@videojs/core/dom'; import type { ReactiveController, ReactiveControllerHost } from '@videojs/element'; import { ContextConsumer } from '@videojs/element/context'; import type { InferStoreState, Selector } from '@videojs/store'; import { StoreController } from '@videojs/store/html'; import type { PlayerContext } from './context'; export type PlayerControllerHost = ReactiveControllerHost & HTMLElement; /** * Reactive controller for accessing player store state. * * Without selector: Returns the store, does NOT subscribe to changes. * With selector: Returns selected state, subscribes with shallowEqual comparison. * * @example * ```ts * // Store access (no subscription) * class Controls extends MediaElement { * #player = new PlayerController(this, playerContext); * * handleClick() { * this.#player.value.setVolume(0.5); * } * } * * // Selector-based subscription * class PlayButton extends MediaElement { * #playback = new PlayerController(this, playerContext, selectPlayback); * } * ``` */ export class PlayerController implements ReactiveController { readonly #host: PlayerControllerHost; readonly #selector: Selector, Result> | undefined; #consumer: ContextConsumer, PlayerControllerHost>; #store: StoreController | null = null; /** * @label Without Selector * @param host - The host element that owns this controller. * @param context - Player context to resolve the store from. */ constructor(host: PlayerControllerHost, context: PlayerContext); /** * @label With Selector * @param host - The host element that owns this controller. * @param context - Player context to resolve the store from. * @param selector - Derives a value from the player store state. */ constructor( host: PlayerControllerHost, context: PlayerContext, selector: Selector, Result> ); constructor( host: PlayerControllerHost, context: PlayerContext, selector?: Selector, Result> ) { this.#host = host; this.#selector = selector; this.#consumer = new ContextConsumer(host, { context, callback: (ctx) => this.#connect(ctx), subscribe: true, }); host.addController(this); } get value(): Result | undefined { const store = this.#consumer.value; if (!store) return undefined; // Without selector: return store directly if (!this.#selector) return store as unknown as Result; // With selector: use StoreController return this.#store?.value; } get displayName(): string | undefined { return this.#selector?.displayName; } hostConnected(): void { const store = this.#consumer.value; if (store) this.#connect(store); } hostDisconnected(): void { this.#store = null; } #connect(store: Store): void { if (!this.#store && this.#selector) { this.#store = new StoreController(this.#host, store, this.#selector); } } } export namespace PlayerController { export type Host = PlayerControllerHost; export type Constructor = typeof PlayerController< Store, Result >; }