UNPKG

unified-video-framework

Version:

Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more

354 lines (296 loc) 9.28 kB
/** * Controller for skip button UI and interactions */ import { VideoSegment, SkipButtonState, SkipButtonPosition, ChapterConfig, DEFAULT_SKIP_LABELS } from './types/ChapterTypes'; export class SkipButtonController { private skipButton: HTMLElement | null = null; private currentSegment: VideoSegment | null = null; private autoSkipTimeout: NodeJS.Timeout | null = null; private hideTimeout: NodeJS.Timeout | null = null; private countdownInterval: NodeJS.Timeout | null = null; private state: SkipButtonState; constructor( private playerContainer: HTMLElement, private config: ChapterConfig, private onSkip: (segment: VideoSegment) => void, private onButtonShown: (segment: VideoSegment) => void, private onButtonHidden: (segment: VideoSegment, reason: string) => void ) { this.state = { visible: false, segment: null, position: config.skipButtonPosition || 'bottom-right' }; } /** * Show skip button for a segment */ public showSkipButton(segment: VideoSegment, currentTime: number): void { // Check if skip buttons are disabled in preferences if (!this.config.userPreferences?.showSkipButtons) { return; } // Check if this specific segment should show a skip button if (segment.showSkipButton === false) { return; } this.currentSegment = segment; this.state.segment = segment; // Create button if it doesn't exist if (!this.skipButton) { this.skipButton = this.createSkipButton(); this.playerContainer.appendChild(this.skipButton); } // Update button content and show it this.updateSkipButton(segment); this.showButton(); // Handle auto-skip functionality this.handleAutoSkip(segment, currentTime); // Handle auto-hide functionality this.handleAutoHide(); // Emit event this.onButtonShown(segment); } /** * Hide skip button */ public hideSkipButton(reason: 'timeout' | 'segment-end' | 'user-action' | 'manual' = 'manual'): void { if (!this.skipButton || !this.state.visible) { return; } this.hideButton(); this.clearTimeouts(); // Emit event if (this.currentSegment) { this.onButtonHidden(this.currentSegment, reason); } this.state.visible = false; this.state.segment = null; this.currentSegment = null; } /** * Update skip button position */ public updatePosition(position: SkipButtonPosition): void { this.state.position = position; if (this.skipButton) { this.applyPositionStyles(this.skipButton, position); } } /** * Check if button is currently visible */ public isVisible(): boolean { return this.state.visible; } /** * Get current button state */ public getState(): SkipButtonState { return { ...this.state }; } /** * Destroy the skip button controller */ public destroy(): void { this.clearTimeouts(); if (this.skipButton) { this.skipButton.remove(); this.skipButton = null; } this.currentSegment = null; this.state.visible = false; this.state.segment = null; } /** * Create the skip button DOM element */ private createSkipButton(): HTMLElement { const button = document.createElement('button'); button.className = 'uvf-skip-button'; button.setAttribute('type', 'button'); button.setAttribute('aria-label', 'Skip segment'); // Apply position styles this.applyPositionStyles(button, this.state.position); // Add click handler button.addEventListener('click', () => { if (this.currentSegment) { this.onSkip(this.currentSegment); this.hideSkipButton('user-action'); } }); // Apply custom styles if provided if (this.config.customStyles?.skipButton) { Object.assign(button.style, this.config.customStyles.skipButton); } return button; } /** * Update skip button content for current segment */ private updateSkipButton(segment: VideoSegment): void { if (!this.skipButton) return; // Set button text const skipLabel = segment.skipLabel || DEFAULT_SKIP_LABELS[segment.type]; this.skipButton.textContent = skipLabel; // Update aria-label for accessibility this.skipButton.setAttribute('aria-label', `${skipLabel} - ${segment.title || segment.type}`); // Add segment type class for styling this.skipButton.className = `uvf-skip-button uvf-skip-${segment.type}`; // Apply position class this.skipButton.classList.add(`uvf-skip-button-${this.state.position}`); } /** * Apply position styles to skip button */ private applyPositionStyles(button: HTMLElement, position: SkipButtonPosition): void { // Reset position classes button.classList.remove( 'uvf-skip-button-bottom-right', 'uvf-skip-button-bottom-left', 'uvf-skip-button-top-right', 'uvf-skip-button-top-left' ); // Add new position class button.classList.add(`uvf-skip-button-${position}`); // Apply CSS styles based on position const styles: Partial<CSSStyleDeclaration> = { position: 'absolute', zIndex: '1000' }; switch (position) { case 'bottom-right': Object.assign(styles, { bottom: '100px', right: '30px' }); break; case 'bottom-left': Object.assign(styles, { bottom: '100px', left: '30px' }); break; case 'top-right': Object.assign(styles, { top: '30px', right: '30px' }); break; case 'top-left': Object.assign(styles, { top: '30px', left: '30px' }); break; } Object.assign(button.style, styles); } /** * Show the skip button with animation */ private showButton(): void { if (!this.skipButton) return; this.skipButton.classList.add('visible'); this.state.visible = true; } /** * Hide the skip button with animation */ private hideButton(): void { if (!this.skipButton) return; this.skipButton.classList.remove('visible'); this.skipButton.classList.remove('auto-skip', 'countdown'); } /** * Handle auto-skip functionality */ private handleAutoSkip(segment: VideoSegment, currentTime: number): void { if (!segment.autoSkip || !segment.autoSkipDelay) { return; } // Check user preferences for auto-skip const preferences = this.config.userPreferences; const shouldAutoSkip = ( (segment.type === 'intro' && preferences?.autoSkipIntro) || (segment.type === 'recap' && preferences?.autoSkipRecap) || (segment.type === 'credits' && preferences?.autoSkipCredits) ); if (!shouldAutoSkip) { return; } // Add auto-skip class for styling this.skipButton?.classList.add('auto-skip'); // Start countdown this.startAutoSkipCountdown(segment, segment.autoSkipDelay); } /** * Start auto-skip countdown */ private startAutoSkipCountdown(segment: VideoSegment, delay: number): void { if (!this.skipButton) return; let remainingTime = delay; this.state.autoSkipCountdown = remainingTime; // Update button text with countdown const originalText = this.skipButton.textContent || ''; // Start countdown animation this.skipButton.classList.add('countdown'); // Update countdown every second this.countdownInterval = setInterval(() => { remainingTime -= 1; this.state.autoSkipCountdown = remainingTime; if (this.skipButton) { this.skipButton.textContent = `${originalText} (${remainingTime})`; } if (remainingTime <= 0) { this.clearTimeouts(); if (this.currentSegment) { this.onSkip(this.currentSegment); this.hideSkipButton('timeout'); } } }, 1000); // Set final timeout as backup this.autoSkipTimeout = setTimeout(() => { if (this.currentSegment) { this.onSkip(this.currentSegment); this.hideSkipButton('timeout'); } }, delay * 1000); } /** * Handle auto-hide functionality */ private handleAutoHide(): void { if (!this.config.autoHide || !this.config.autoHideDelay) { return; } this.hideTimeout = setTimeout(() => { this.hideSkipButton('timeout'); }, this.config.autoHideDelay); } /** * Clear all timeouts */ private clearTimeouts(): void { if (this.autoSkipTimeout) { clearTimeout(this.autoSkipTimeout); this.autoSkipTimeout = null; } if (this.hideTimeout) { clearTimeout(this.hideTimeout); this.hideTimeout = null; } if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } this.state.autoSkipCountdown = undefined; } }