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
text/typescript
/**
* 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;
}
}