unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
284 lines • 11.8 kB
JavaScript
export class CreditsButtonController {
constructor(playerContainer, config, callbacks) {
this.playerContainer = playerContainer;
this.config = config;
this.callbacks = callbacks;
this.buttonsContainer = null;
this.watchCreditsButton = null;
this.nextEpisodeButton = null;
this.currentSegment = null;
this.autoRedirectTimeout = null;
this.countdownInterval = null;
this.isVisible = false;
this.userWatchingCredits = false;
this.DEFAULT_WATCH_CREDITS_LABEL = 'Watch Credits';
this.DEFAULT_NEXT_EPISODE_LABEL = 'Play Next';
this.DEFAULT_AUTO_REDIRECT_DELAY = 10;
}
showCreditsButtons(segment, currentTime) {
if (!segment.nextEpisodeUrl) {
return;
}
this.currentSegment = segment;
this.userWatchingCredits = false;
if (!this.buttonsContainer) {
this.createCreditsButtons();
}
this.updateButtonLabels(segment);
this.showButtons();
const delay = segment.autoSkipDelay || this.DEFAULT_AUTO_REDIRECT_DELAY;
this.startAutoRedirectCountdown(segment, delay);
this.callbacks.onButtonsShown(segment);
}
hideCreditsButtons(reason = 'manual') {
if (!this.buttonsContainer || !this.isVisible) {
return;
}
if (this.currentSegment) {
this.callbacks.onButtonsHidden(this.currentSegment, reason);
}
this.hideButtons();
this.clearTimeouts();
this.isVisible = false;
this.currentSegment = null;
}
isUserWatchingCredits() {
return this.userWatchingCredits;
}
getCurrentSegment() {
return this.currentSegment;
}
isButtonsVisible() {
return this.isVisible;
}
destroy() {
this.clearTimeouts();
if (this.buttonsContainer) {
this.buttonsContainer.remove();
this.buttonsContainer = null;
}
this.watchCreditsButton = null;
this.nextEpisodeButton = null;
this.currentSegment = null;
this.isVisible = false;
this.userWatchingCredits = false;
}
createCreditsButtons() {
const segment = this.currentSegment;
const style = segment?.creditsButtonStyle;
const layout = style?.layout || 'vertical';
const position = style?.position || 'bottom-right';
this.buttonsContainer = document.createElement('div');
this.buttonsContainer.className = 'uvf-credits-buttons';
this.buttonsContainer.setAttribute('role', 'group');
this.buttonsContainer.setAttribute('aria-label', 'Credits navigation');
const containerStyles = {
position: 'absolute',
display: 'flex',
flexDirection: layout === 'horizontal' ? 'row' : 'column',
gap: '10px',
zIndex: '1000',
opacity: '0',
transition: 'opacity 0.3s ease-in-out'
};
this.applyContainerPosition(containerStyles, position);
Object.assign(this.buttonsContainer.style, containerStyles);
this.watchCreditsButton = document.createElement('button');
this.watchCreditsButton.className = 'uvf-watch-credits-button';
this.watchCreditsButton.setAttribute('type', 'button');
this.watchCreditsButton.setAttribute('aria-label', 'Watch credits');
this.watchCreditsButton.addEventListener('click', () => this.handleWatchCreditsClick());
this.nextEpisodeButton = document.createElement('button');
this.nextEpisodeButton.className = 'uvf-next-episode-button';
this.nextEpisodeButton.setAttribute('type', 'button');
this.nextEpisodeButton.setAttribute('aria-label', 'Play next episode');
this.nextEpisodeButton.addEventListener('click', () => this.handleNextEpisodeClick());
this.applyButtonStyles(this.watchCreditsButton, 'watch-credits', style);
this.applyButtonStyles(this.nextEpisodeButton, 'next-episode', style);
this.buttonsContainer.appendChild(this.watchCreditsButton);
this.buttonsContainer.appendChild(this.nextEpisodeButton);
this.playerContainer.appendChild(this.buttonsContainer);
}
applyContainerPosition(styles, position) {
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 'bottom-center':
Object.assign(styles, { bottom: '100px', left: '50%', transform: 'translateX(-50%)' });
break;
case 'top-right':
Object.assign(styles, { top: '30px', right: '30px' });
break;
case 'top-left':
Object.assign(styles, { top: '30px', left: '30px' });
break;
case 'top-center':
Object.assign(styles, { top: '30px', left: '50%', transform: 'translateX(-50%)' });
break;
}
}
applyButtonStyles(button, type, customStyle) {
const baseStyles = {
padding: '12px 24px',
fontSize: '14px',
fontWeight: '600',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
fontFamily: 'inherit',
outline: 'none',
minWidth: '180px',
position: 'relative'
};
if (type === 'watch-credits') {
Object.assign(baseStyles, {
backgroundColor: customStyle?.watchCreditsBgColor || 'rgba(255, 255, 255, 0.15)',
color: customStyle?.watchCreditsColor || '#ffffff',
border: `2px solid ${customStyle?.watchCreditsBgColor ? 'transparent' : 'rgba(255, 255, 255, 0.3)'}`
});
}
else {
Object.assign(baseStyles, {
backgroundColor: customStyle?.nextEpisodeBgColor || '#e50914',
color: customStyle?.nextEpisodeColor || '#ffffff',
boxShadow: '0 2px 8px rgba(229, 9, 20, 0.4)'
});
}
Object.assign(button.style, baseStyles);
button.addEventListener('mouseenter', () => {
if (type === 'watch-credits') {
button.style.backgroundColor = customStyle?.watchCreditsBgColor
? this.adjustBrightness(customStyle.watchCreditsBgColor, 20)
: 'rgba(255, 255, 255, 0.25)';
}
else {
button.style.backgroundColor = customStyle?.nextEpisodeBgColor
? this.adjustBrightness(customStyle.nextEpisodeBgColor, 20)
: '#f40612';
button.style.transform = 'scale(1.03)';
}
});
button.addEventListener('mouseleave', () => {
if (type === 'watch-credits') {
button.style.backgroundColor = customStyle?.watchCreditsBgColor || 'rgba(255, 255, 255, 0.15)';
}
else {
button.style.backgroundColor = customStyle?.nextEpisodeBgColor || '#e50914';
button.style.transform = 'scale(1)';
}
});
}
adjustBrightness(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255))
.toString(16).slice(1);
}
updateButtonLabels(segment) {
if (!this.watchCreditsButton || !this.nextEpisodeButton)
return;
const watchLabel = segment.watchCreditsLabel || this.DEFAULT_WATCH_CREDITS_LABEL;
const nextLabel = segment.nextEpisodeLabel || this.DEFAULT_NEXT_EPISODE_LABEL;
this.watchCreditsButton.textContent = watchLabel;
this.nextEpisodeButton.textContent = nextLabel;
this.watchCreditsButton.setAttribute('aria-label', watchLabel);
this.nextEpisodeButton.setAttribute('aria-label', nextLabel);
}
showButtons() {
if (!this.buttonsContainer)
return;
this.buttonsContainer.style.opacity = '1';
this.isVisible = true;
}
hideButtons() {
if (!this.buttonsContainer)
return;
this.buttonsContainer.remove();
this.buttonsContainer = null;
this.watchCreditsButton = null;
this.nextEpisodeButton = null;
}
handleWatchCreditsClick() {
if (!this.currentSegment)
return;
this.userWatchingCredits = true;
this.clearTimeouts();
this.hideCreditsButtons('user-action');
this.callbacks.onWatchCredits(this.currentSegment);
}
handleNextEpisodeClick() {
if (!this.currentSegment || !this.currentSegment.nextEpisodeUrl)
return;
this.clearTimeouts();
this.callbacks.onNextEpisode(this.currentSegment, this.currentSegment.nextEpisodeUrl);
this.redirectToNextEpisode(this.currentSegment.nextEpisodeUrl);
}
startAutoRedirectCountdown(segment, delay) {
if (!this.nextEpisodeButton || !segment.nextEpisodeUrl)
return;
let remainingTime = delay;
const originalLabel = segment.nextEpisodeLabel || this.DEFAULT_NEXT_EPISODE_LABEL;
this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
const progressBar = this.createProgressBar();
this.nextEpisodeButton.appendChild(progressBar);
this.countdownInterval = setInterval(() => {
remainingTime -= 1;
if (this.nextEpisodeButton) {
this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
this.nextEpisodeButton.appendChild(progressBar);
const progress = ((delay - remainingTime) / delay) * 100;
progressBar.style.width = `${progress}%`;
}
if (remainingTime <= 0) {
this.clearTimeouts();
this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl);
this.redirectToNextEpisode(segment.nextEpisodeUrl);
}
}, 1000);
this.autoRedirectTimeout = setTimeout(() => {
if (segment.nextEpisodeUrl) {
this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl);
this.redirectToNextEpisode(segment.nextEpisodeUrl);
}
}, delay * 1000);
}
createProgressBar() {
const progressBar = document.createElement('div');
progressBar.className = 'uvf-countdown-progress';
Object.assign(progressBar.style, {
position: 'absolute',
bottom: '0',
left: '0',
height: '3px',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
width: '0%',
transition: 'width 1s linear',
borderRadius: '6px'
});
return progressBar;
}
redirectToNextEpisode(url) {
window.location.href = url;
}
clearTimeouts() {
if (this.autoRedirectTimeout) {
clearTimeout(this.autoRedirectTimeout);
this.autoRedirectTimeout = null;
}
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
}
}
//# sourceMappingURL=CreditsButtonController.js.map