import { setLucideIcon } from './icons.js'; import { formatTime } from '../utils/time.js'; type TwoDControlPanelCallbacks = { onForward: () => void; onMute: () => void; onPlayPause: () => void; onRewind: () => void; onSeek: (progress: number) => void; }; type TwoDControlPanelOptions = { callbacks: TwoDControlPanelCallbacks; fullscreenTarget: HTMLElement; getIsActive: () => boolean; playerContainer: HTMLElement; title: string; }; const CONTROL_PANEL_HIDE_DELAY = 3000; export class TwoDControlPanel { private readonly callbacks: TwoDControlPanelCallbacks; private readonly fullscreenTarget: HTMLElement; private readonly getIsActive: () => boolean; private readonly playerContainer: HTMLElement; private controlPanel: HTMLElement | null; private currentTimeDisplay: HTMLElement | null; private hideTimeout: number | undefined; private playedBar: HTMLElement | null; private progressBar: HTMLElement | null; private totalTimeDisplay: HTMLElement | null; private playButton: HTMLButtonElement | null; private muteButton: HTMLButtonElement | null; constructor({ callbacks, fullscreenTarget, getIsActive, playerContainer, title }: TwoDControlPanelOptions) { this.callbacks = callbacks; this.fullscreenTarget = fullscreenTarget; this.getIsActive = getIsActive; this.playerContainer = playerContainer; this.controlPanel = playerContainer.querySelector('.vrwp-panel'); const videoTitle = playerContainer.querySelector('.vrwp-video-title'); this.currentTimeDisplay = playerContainer.querySelector('.vrwp-current-time'); this.totalTimeDisplay = playerContainer.querySelector('.vrwp-total-time'); this.progressBar = playerContainer.querySelector('.vrwp-bar'); this.playedBar = playerContainer.querySelector('.vrwp-played'); this.playButton = playerContainer.querySelector('.vrwp-play-toggle'); this.muteButton = playerContainer.querySelector('.vrwp-mute'); if (!this.controlPanel) { console.error('VR_WEB_PLAYER_DOM: 2D control panel was not found.'); return; } if (videoTitle) { videoTitle.textContent = title; } this.bindControls(playerContainer); } show(): void { if (!this.getIsActive() || !this.controlPanel) return; this.clearHideTimeout(); this.controlPanel.classList.add('visible'); this.hideTimeout = window.setTimeout(() => this.hide(), CONTROL_PANEL_HIDE_DELAY); } showPersistent(): void { if (!this.getIsActive() || !this.controlPanel) return; this.clearHideTimeout(); this.controlPanel.classList.add('visible'); } hide(): void { if (!this.controlPanel) return; this.clearHideTimeout(); this.controlPanel.classList.remove('visible'); } position(canvas: HTMLElement): void { if (!this.getIsActive() || !this.controlPanel) return; const canvasRect = canvas.getBoundingClientRect(); const containerRect = this.playerContainer.getBoundingClientRect(); const bottomOffset = canvasRect.height * 0.1; const panelHeight = this.controlPanel.offsetHeight; const topPosition = (canvasRect.bottom - containerRect.top) - bottomOffset - panelHeight; this.controlPanel.style.position = 'absolute'; this.controlPanel.style.top = `${topPosition}px`; this.controlPanel.style.bottom = 'auto'; this.controlPanel.style.left = '50%'; this.controlPanel.style.transform = 'translateX(-50%)'; this.controlPanel.style.zIndex = '1000'; } updateMuteButton(isMuted: boolean): void { if (!this.getIsActive() || !this.muteButton) return; if (isMuted) { this.muteButton.classList.remove('muted'); this.muteButton.classList.add('unmuted'); setLucideIcon(this.muteButton, 'volume-x'); } else { this.muteButton.classList.remove('unmuted'); this.muteButton.classList.add('muted'); setLucideIcon(this.muteButton, 'volume-2'); } } updatePlaybackButton(isPausedOrEnded: boolean): void { if (!this.getIsActive() || !this.playButton) return; if (isPausedOrEnded) { this.playButton.classList.remove('playing'); this.playButton.classList.add('paused'); setLucideIcon(this.playButton, 'play'); } else { this.playButton.classList.remove('paused'); this.playButton.classList.add('playing'); setLucideIcon(this.playButton, 'pause'); } } updateTime(currentTime: number, duration: number): void { if (!this.getIsActive()) return; if (this.currentTimeDisplay) { this.currentTimeDisplay.textContent = formatTime(currentTime); } if (this.totalTimeDisplay && isFinite(duration)) { this.totalTimeDisplay.textContent = formatTime(duration); } if (this.playedBar && isFinite(duration) && duration > 0) { const progress = (currentTime / duration) * 100; this.playedBar.style.width = `${progress}%`; } } private bindControls(playerContainer: HTMLElement): void { playerContainer.querySelector('.vrwp-fullscreen')?.addEventListener('click', () => { this.toggleFullscreen(); }); playerContainer.querySelector('.vrwp-back')?.addEventListener('click', () => { this.callbacks.onRewind(); this.show(); }); this.playButton?.addEventListener('click', () => { this.callbacks.onPlayPause(); this.show(); }); playerContainer.querySelector('.vrwp-forward')?.addEventListener('click', () => { this.callbacks.onForward(); this.show(); }); this.muteButton?.addEventListener('click', () => { this.callbacks.onMute(); this.show(); }); this.progressBar?.addEventListener('click', (event) => { const rect = this.progressBar?.getBoundingClientRect(); if (rect && rect.width > 0) { this.callbacks.onSeek((event.clientX - rect.left) / rect.width); } this.show(); }); } private clearHideTimeout(): void { if (this.hideTimeout !== undefined) { clearTimeout(this.hideTimeout); this.hideTimeout = undefined; } } private toggleFullscreen(): void { if (!document.fullscreenElement) { this.fullscreenTarget.requestFullscreen().catch((err) => { console.error('Error attempting to enable fullscreen:', err); }); return; } document.exitFullscreen().catch((err) => { console.error('Error attempting to exit fullscreen:', err); }); } }