unified-video-framework 1.4.365 → 1.4.367
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +0 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/dist/interfaces.d.ts +1 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces/IVideoPlayer.ts +1 -2
- package/packages/core/src/interfaces.ts +4 -1
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +1 -33
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts +4 -3
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.js +51 -34
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
- package/packages/web/dist/chapters/CreditsButtonController.d.ts +44 -0
- package/packages/web/dist/chapters/CreditsButtonController.d.ts.map +1 -0
- package/packages/web/dist/chapters/CreditsButtonController.js +246 -0
- package/packages/web/dist/chapters/CreditsButtonController.js.map +1 -0
- package/packages/web/dist/chapters/SkipButtonController.d.ts +1 -4
- package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -1
- package/packages/web/dist/chapters/SkipButtonController.js +22 -118
- package/packages/web/dist/chapters/SkipButtonController.js.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +21 -12
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +0 -10
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +1 -5
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/react/components/SkipButton.d.ts +0 -2
- package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -1
- package/packages/web/dist/react/components/SkipButton.js +13 -30
- package/packages/web/dist/react/components/SkipButton.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +7475 -7507
- package/packages/web/src/chapters/ChapterManager.ts +77 -57
- package/packages/web/src/chapters/CreditsButtonController.ts +397 -0
- package/packages/web/src/chapters/SkipButtonController.ts +33 -148
- package/packages/web/src/chapters/types/ChapterTypes.ts +34 -19
- package/packages/web/src/react/WebPlayerView.tsx +1 -14
- package/packages/web/src/react/components/SkipButton.tsx +24 -82
|
@@ -13,12 +13,14 @@ import {
|
|
|
13
13
|
SEGMENT_COLORS
|
|
14
14
|
} from './types/ChapterTypes';
|
|
15
15
|
import { SkipButtonController } from './SkipButtonController';
|
|
16
|
+
import { CreditsButtonController } from './CreditsButtonController';
|
|
16
17
|
|
|
17
18
|
export class ChapterManager {
|
|
18
19
|
private chapters: VideoChapters | null = null;
|
|
19
20
|
private currentSegment: VideoSegment | null = null;
|
|
20
21
|
private previousSegment: VideoSegment | null = null;
|
|
21
22
|
private skipButtonController: SkipButtonController;
|
|
23
|
+
private creditsButtonController: CreditsButtonController;
|
|
22
24
|
private config: ChapterConfig;
|
|
23
25
|
private eventListeners: Map<keyof ChapterEvents, Function[]> = new Map();
|
|
24
26
|
private isDestroyed = false;
|
|
@@ -41,8 +43,24 @@ export class ChapterManager {
|
|
|
41
43
|
segment,
|
|
42
44
|
currentTime: this.videoElement.currentTime,
|
|
43
45
|
reason: reason as any
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Initialize credits button controller
|
|
50
|
+
this.creditsButtonController = new CreditsButtonController(
|
|
51
|
+
playerContainer,
|
|
52
|
+
this.config,
|
|
53
|
+
{
|
|
54
|
+
onWatchCredits: (segment) => this.handleWatchCredits(segment),
|
|
55
|
+
onNextEpisode: (segment, url) => this.handleNextEpisode(segment, url),
|
|
56
|
+
onAutoRedirect: (segment, url) => this.handleAutoRedirect(segment, url),
|
|
57
|
+
onButtonsShown: (segment) => this.emit('skipButtonShown', { segment, currentTime: this.videoElement.currentTime }),
|
|
58
|
+
onButtonsHidden: (segment, reason) => this.emit('skipButtonHidden', {
|
|
59
|
+
segment,
|
|
60
|
+
currentTime: this.videoElement.currentTime,
|
|
61
|
+
reason: reason as any
|
|
62
|
+
})
|
|
63
|
+
}
|
|
46
64
|
);
|
|
47
65
|
|
|
48
66
|
// Set up time update listener
|
|
@@ -126,26 +144,9 @@ export class ChapterManager {
|
|
|
126
144
|
* Skip to next segment after current one
|
|
127
145
|
*/
|
|
128
146
|
public skipToNextSegment(currentSegment: VideoSegment): void {
|
|
129
|
-
console.log('[ChapterManager] skipToNextSegment called with segment:', currentSegment.type, currentSegment.title);
|
|
130
147
|
if (!this.chapters) return;
|
|
131
148
|
|
|
132
149
|
const nextSegment = this.getNextContentSegment(currentSegment);
|
|
133
|
-
console.log('[ChapterManager] Next content segment:', nextSegment?.type || 'none');
|
|
134
|
-
|
|
135
|
-
// Check for Next Episode trigger if no next segment or if we are skipping credits
|
|
136
|
-
console.log('[ChapterManager] Checking Next Episode conditions:', {
|
|
137
|
-
noNextSegment: !nextSegment,
|
|
138
|
-
isCredits: currentSegment.type === 'credits',
|
|
139
|
-
hasNextEpisodeConfig: !!(this.config.nextEpisode?.enabled || this.config.nextEpisode?.url)
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if ((!nextSegment || currentSegment.type === 'credits') &&
|
|
143
|
-
(this.config.nextEpisode?.enabled || this.config.nextEpisode?.url)) {
|
|
144
|
-
console.log('[ChapterManager] Triggering Next Episode!');
|
|
145
|
-
this.triggerNextEpisode();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
150
|
const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime;
|
|
150
151
|
|
|
151
152
|
// Store current playback state
|
|
@@ -176,32 +177,6 @@ export class ChapterManager {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
/**
|
|
180
|
-
* Handle secondary action (Watch Credits)
|
|
181
|
-
*/
|
|
182
|
-
public handleSecondaryAction(segment: VideoSegment): void {
|
|
183
|
-
// For "Watch Credits", we imply that the user wants to stay on the credits.
|
|
184
|
-
// So we just emit an event or log it, but importantly the SkipButton will have
|
|
185
|
-
// cancelled the auto-skip.
|
|
186
|
-
console.log('[ChapterManager] User chose to watch credits');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Trigger next episode directly
|
|
191
|
-
*/
|
|
192
|
-
public triggerNextEpisode(): void {
|
|
193
|
-
console.log('[ChapterManager] triggerNextEpisode called. nextEpisode config:', this.config.nextEpisode);
|
|
194
|
-
if (this.config.nextEpisode?.enabled || this.config.nextEpisode?.url) {
|
|
195
|
-
console.log('[ChapterManager] Emitting nextEpisode event with config:', this.config.nextEpisode);
|
|
196
|
-
this.emit('nextEpisode', {
|
|
197
|
-
config: this.config.nextEpisode,
|
|
198
|
-
autoPlay: true
|
|
199
|
-
});
|
|
200
|
-
} else {
|
|
201
|
-
console.log('[ChapterManager] nextEpisode not triggered - no enabled flag or URL');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
180
|
/**
|
|
206
181
|
* Skip to specific segment by ID
|
|
207
182
|
*/
|
|
@@ -338,6 +313,7 @@ export class ChapterManager {
|
|
|
338
313
|
public destroy(): void {
|
|
339
314
|
this.isDestroyed = true;
|
|
340
315
|
this.skipButtonController.destroy();
|
|
316
|
+
this.creditsButtonController.destroy();
|
|
341
317
|
this.removeChapterMarkers();
|
|
342
318
|
this.eventListeners.clear();
|
|
343
319
|
this.chapters = null;
|
|
@@ -393,6 +369,28 @@ export class ChapterManager {
|
|
|
393
369
|
if (this.shouldShowSkipButton(this.currentSegment)) {
|
|
394
370
|
this.skipButtonController.hideSkipButton('segment-end');
|
|
395
371
|
}
|
|
372
|
+
|
|
373
|
+
// Hide credits buttons when exiting credits segment
|
|
374
|
+
if (this.currentSegment.type === 'credits' && this.currentSegment.nextEpisodeUrl) {
|
|
375
|
+
this.creditsButtonController.hideCreditsButtons('segment-end');
|
|
376
|
+
|
|
377
|
+
// If user watched full credits, redirect to next episode
|
|
378
|
+
if (this.creditsButtonController.isUserWatchingCredits()) {
|
|
379
|
+
// Capture URL before segment state changes
|
|
380
|
+
const redirectUrl = this.currentSegment.nextEpisodeUrl;
|
|
381
|
+
|
|
382
|
+
this.emit('creditsFullyWatched', {
|
|
383
|
+
segment: this.currentSegment,
|
|
384
|
+
nextEpisodeUrl: redirectUrl,
|
|
385
|
+
currentTime
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Redirect to next episode after short delay
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
window.location.href = redirectUrl;
|
|
391
|
+
}, 500);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
396
394
|
}
|
|
397
395
|
|
|
398
396
|
// Update current segment
|
|
@@ -407,8 +405,12 @@ export class ChapterManager {
|
|
|
407
405
|
previousSegment: this.previousSegment || undefined
|
|
408
406
|
});
|
|
409
407
|
|
|
410
|
-
//
|
|
411
|
-
if (this.
|
|
408
|
+
// Check if this is a credits segment with next episode URL
|
|
409
|
+
if (this.currentSegment.type === 'credits' && this.currentSegment.nextEpisodeUrl) {
|
|
410
|
+
// Show credits buttons instead of skip button
|
|
411
|
+
this.creditsButtonController.showCreditsButtons(this.currentSegment, currentTime);
|
|
412
|
+
} else if (this.shouldShowSkipButton(this.currentSegment)) {
|
|
413
|
+
// Show skip button for regular skippable segments
|
|
412
414
|
this.skipButtonController.showSkipButton(this.currentSegment, currentTime);
|
|
413
415
|
}
|
|
414
416
|
}
|
|
@@ -429,17 +431,35 @@ export class ChapterManager {
|
|
|
429
431
|
}
|
|
430
432
|
|
|
431
433
|
/**
|
|
432
|
-
*
|
|
434
|
+
* Handle "Watch Credits" button click
|
|
433
435
|
*/
|
|
434
|
-
private
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
436
|
+
private handleWatchCredits(segment: VideoSegment): void {
|
|
437
|
+
this.emit('creditsWatched', {
|
|
438
|
+
segment,
|
|
439
|
+
currentTime: this.videoElement.currentTime
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Handle "Next Episode" button click
|
|
445
|
+
*/
|
|
446
|
+
private handleNextEpisode(segment: VideoSegment, url: string): void {
|
|
447
|
+
this.emit('nextEpisodeClicked', {
|
|
448
|
+
segment,
|
|
449
|
+
nextEpisodeUrl: url,
|
|
450
|
+
currentTime: this.videoElement.currentTime
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Handle auto-redirect when countdown expires
|
|
456
|
+
*/
|
|
457
|
+
private handleAutoRedirect(segment: VideoSegment, url: string): void {
|
|
458
|
+
this.emit('creditsAutoRedirect', {
|
|
459
|
+
segment,
|
|
460
|
+
nextEpisodeUrl: url,
|
|
461
|
+
currentTime: this.videoElement.currentTime
|
|
462
|
+
});
|
|
443
463
|
}
|
|
444
464
|
|
|
445
465
|
/**
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller for credits dual-button UI (Watch Credits & Next Episode)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
VideoSegment,
|
|
7
|
+
ChapterConfig
|
|
8
|
+
} from './types/ChapterTypes';
|
|
9
|
+
|
|
10
|
+
interface CreditsButtonCallbacks {
|
|
11
|
+
onWatchCredits: (segment: VideoSegment) => void;
|
|
12
|
+
onNextEpisode: (segment: VideoSegment, url: string) => void;
|
|
13
|
+
onAutoRedirect: (segment: VideoSegment, url: string) => void;
|
|
14
|
+
onButtonsShown: (segment: VideoSegment) => void;
|
|
15
|
+
onButtonsHidden: (segment: VideoSegment, reason: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CreditsButtonController {
|
|
19
|
+
private buttonsContainer: HTMLElement | null = null;
|
|
20
|
+
private watchCreditsButton: HTMLElement | null = null;
|
|
21
|
+
private nextEpisodeButton: HTMLElement | null = null;
|
|
22
|
+
private currentSegment: VideoSegment | null = null;
|
|
23
|
+
private autoRedirectTimeout: NodeJS.Timeout | null = null;
|
|
24
|
+
private countdownInterval: NodeJS.Timeout | null = null;
|
|
25
|
+
private isVisible = false;
|
|
26
|
+
private userWatchingCredits = false;
|
|
27
|
+
|
|
28
|
+
// Default labels
|
|
29
|
+
private readonly DEFAULT_WATCH_CREDITS_LABEL = 'Watch Credits';
|
|
30
|
+
private readonly DEFAULT_NEXT_EPISODE_LABEL = 'Play Next';
|
|
31
|
+
private readonly DEFAULT_AUTO_REDIRECT_DELAY = 10; // seconds
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private playerContainer: HTMLElement,
|
|
35
|
+
private config: ChapterConfig,
|
|
36
|
+
private callbacks: CreditsButtonCallbacks
|
|
37
|
+
) { }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Show credits buttons for a segment with next episode URL
|
|
41
|
+
*/
|
|
42
|
+
public showCreditsButtons(segment: VideoSegment, currentTime: number): void {
|
|
43
|
+
// Verify segment has nextEpisodeUrl
|
|
44
|
+
if (!segment.nextEpisodeUrl) {
|
|
45
|
+
console.warn('[CreditsButtonController] Segment does not have nextEpisodeUrl');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.currentSegment = segment;
|
|
50
|
+
this.userWatchingCredits = false;
|
|
51
|
+
|
|
52
|
+
// Create buttons if they don't exist
|
|
53
|
+
if (!this.buttonsContainer) {
|
|
54
|
+
this.createCreditsButtons();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update button content
|
|
58
|
+
this.updateButtonLabels(segment);
|
|
59
|
+
|
|
60
|
+
// Show the buttons
|
|
61
|
+
this.showButtons();
|
|
62
|
+
|
|
63
|
+
// Start auto-redirect countdown
|
|
64
|
+
const delay = segment.autoSkipDelay || this.DEFAULT_AUTO_REDIRECT_DELAY;
|
|
65
|
+
this.startAutoRedirectCountdown(segment, delay);
|
|
66
|
+
|
|
67
|
+
// Emit event
|
|
68
|
+
this.callbacks.onButtonsShown(segment);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Hide credits buttons
|
|
73
|
+
*/
|
|
74
|
+
public hideCreditsButtons(reason: 'timeout' | 'segment-end' | 'user-action' | 'manual' = 'manual'): void {
|
|
75
|
+
if (!this.buttonsContainer || !this.isVisible) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Emit event before cleanup
|
|
80
|
+
if (this.currentSegment) {
|
|
81
|
+
this.callbacks.onButtonsHidden(this.currentSegment, reason);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.hideButtons();
|
|
85
|
+
this.clearTimeouts();
|
|
86
|
+
|
|
87
|
+
this.isVisible = false;
|
|
88
|
+
this.currentSegment = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if user is watching credits (clicked "Watch Credits")
|
|
93
|
+
*/
|
|
94
|
+
public isUserWatchingCredits(): boolean {
|
|
95
|
+
return this.userWatchingCredits;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get current segment
|
|
100
|
+
*/
|
|
101
|
+
public getCurrentSegment(): VideoSegment | null {
|
|
102
|
+
return this.currentSegment;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if buttons are visible
|
|
107
|
+
*/
|
|
108
|
+
public isButtonsVisible(): boolean {
|
|
109
|
+
return this.isVisible;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Destroy the credits button controller
|
|
114
|
+
*/
|
|
115
|
+
public destroy(): void {
|
|
116
|
+
this.clearTimeouts();
|
|
117
|
+
if (this.buttonsContainer) {
|
|
118
|
+
this.buttonsContainer.remove();
|
|
119
|
+
this.buttonsContainer = null;
|
|
120
|
+
}
|
|
121
|
+
this.watchCreditsButton = null;
|
|
122
|
+
this.nextEpisodeButton = null;
|
|
123
|
+
this.currentSegment = null;
|
|
124
|
+
this.isVisible = false;
|
|
125
|
+
this.userWatchingCredits = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create the credits buttons DOM elements
|
|
130
|
+
*/
|
|
131
|
+
private createCreditsButtons(): void {
|
|
132
|
+
// Create container
|
|
133
|
+
this.buttonsContainer = document.createElement('div');
|
|
134
|
+
this.buttonsContainer.className = 'uvf-credits-buttons';
|
|
135
|
+
this.buttonsContainer.setAttribute('role', 'group');
|
|
136
|
+
this.buttonsContainer.setAttribute('aria-label', 'Credits navigation');
|
|
137
|
+
|
|
138
|
+
// Apply container styles
|
|
139
|
+
Object.assign(this.buttonsContainer.style, {
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
bottom: '100px',
|
|
142
|
+
right: '30px',
|
|
143
|
+
display: 'flex',
|
|
144
|
+
flexDirection: 'column',
|
|
145
|
+
gap: '10px',
|
|
146
|
+
zIndex: '1000',
|
|
147
|
+
opacity: '0',
|
|
148
|
+
transition: 'opacity 0.3s ease-in-out'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Create "Watch Credits" button
|
|
152
|
+
this.watchCreditsButton = document.createElement('button');
|
|
153
|
+
this.watchCreditsButton.className = 'uvf-watch-credits-button';
|
|
154
|
+
this.watchCreditsButton.setAttribute('type', 'button');
|
|
155
|
+
this.watchCreditsButton.setAttribute('aria-label', 'Watch credits');
|
|
156
|
+
|
|
157
|
+
// Add click handler for Watch Credits
|
|
158
|
+
this.watchCreditsButton.addEventListener('click', () => this.handleWatchCreditsClick());
|
|
159
|
+
|
|
160
|
+
// Create "Next Episode" button
|
|
161
|
+
this.nextEpisodeButton = document.createElement('button');
|
|
162
|
+
this.nextEpisodeButton.className = 'uvf-next-episode-button';
|
|
163
|
+
this.nextEpisodeButton.setAttribute('type', 'button');
|
|
164
|
+
this.nextEpisodeButton.setAttribute('aria-label', 'Play next episode');
|
|
165
|
+
|
|
166
|
+
// Add click handler for Next Episode
|
|
167
|
+
this.nextEpisodeButton.addEventListener('click', () => this.handleNextEpisodeClick());
|
|
168
|
+
|
|
169
|
+
// Apply button styles
|
|
170
|
+
this.applyButtonStyles(this.watchCreditsButton, 'watch-credits');
|
|
171
|
+
this.applyButtonStyles(this.nextEpisodeButton, 'next-episode');
|
|
172
|
+
|
|
173
|
+
// Append buttons to container
|
|
174
|
+
this.buttonsContainer.appendChild(this.watchCreditsButton);
|
|
175
|
+
this.buttonsContainer.appendChild(this.nextEpisodeButton);
|
|
176
|
+
|
|
177
|
+
// Append container to player
|
|
178
|
+
this.playerContainer.appendChild(this.buttonsContainer);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply styles to buttons
|
|
183
|
+
*/
|
|
184
|
+
private applyButtonStyles(button: HTMLElement, type: 'watch-credits' | 'next-episode'): void {
|
|
185
|
+
const baseStyles: Partial<CSSStyleDeclaration> = {
|
|
186
|
+
padding: '12px 24px',
|
|
187
|
+
fontSize: '14px',
|
|
188
|
+
fontWeight: '600',
|
|
189
|
+
border: 'none',
|
|
190
|
+
borderRadius: '6px',
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
transition: 'all 0.2s ease-in-out',
|
|
193
|
+
fontFamily: 'inherit',
|
|
194
|
+
outline: 'none',
|
|
195
|
+
minWidth: '180px',
|
|
196
|
+
position: 'relative'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (type === 'watch-credits') {
|
|
200
|
+
Object.assign(baseStyles, {
|
|
201
|
+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
|
202
|
+
color: '#ffffff',
|
|
203
|
+
border: '2px solid rgba(255, 255, 255, 0.3)'
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
Object.assign(baseStyles, {
|
|
207
|
+
backgroundColor: '#e50914',
|
|
208
|
+
color: '#ffffff',
|
|
209
|
+
boxShadow: '0 2px 8px rgba(229, 9, 20, 0.4)'
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Object.assign(button.style, baseStyles);
|
|
214
|
+
|
|
215
|
+
// Add hover effect
|
|
216
|
+
button.addEventListener('mouseenter', () => {
|
|
217
|
+
if (type === 'watch-credits') {
|
|
218
|
+
button.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';
|
|
219
|
+
button.style.borderColor = 'rgba(255, 255, 255, 0.5)';
|
|
220
|
+
} else {
|
|
221
|
+
button.style.backgroundColor = '#f40612';
|
|
222
|
+
button.style.transform = 'scale(1.03)';
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
button.addEventListener('mouseleave', () => {
|
|
227
|
+
if (type === 'watch-credits') {
|
|
228
|
+
button.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
|
|
229
|
+
button.style.borderColor = 'rgba(255, 255, 255, 0.3)';
|
|
230
|
+
} else {
|
|
231
|
+
button.style.backgroundColor = '#e50914';
|
|
232
|
+
button.style.transform = 'scale(1)';
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update button labels based on segment configuration
|
|
239
|
+
*/
|
|
240
|
+
private updateButtonLabels(segment: VideoSegment): void {
|
|
241
|
+
if (!this.watchCreditsButton || !this.nextEpisodeButton) return;
|
|
242
|
+
|
|
243
|
+
const watchLabel = segment.watchCreditsLabel || this.DEFAULT_WATCH_CREDITS_LABEL;
|
|
244
|
+
const nextLabel = segment.nextEpisodeLabel || this.DEFAULT_NEXT_EPISODE_LABEL;
|
|
245
|
+
|
|
246
|
+
this.watchCreditsButton.textContent = watchLabel;
|
|
247
|
+
this.nextEpisodeButton.textContent = nextLabel;
|
|
248
|
+
|
|
249
|
+
this.watchCreditsButton.setAttribute('aria-label', watchLabel);
|
|
250
|
+
this.nextEpisodeButton.setAttribute('aria-label', nextLabel);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Show buttons with animation
|
|
255
|
+
*/
|
|
256
|
+
private showButtons(): void {
|
|
257
|
+
if (!this.buttonsContainer) return;
|
|
258
|
+
|
|
259
|
+
this.buttonsContainer.style.opacity = '1';
|
|
260
|
+
this.isVisible = true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Hide buttons with animation
|
|
265
|
+
*/
|
|
266
|
+
private hideButtons(): void {
|
|
267
|
+
if (!this.buttonsContainer) return;
|
|
268
|
+
|
|
269
|
+
// Remove from DOM instead of just hiding with opacity
|
|
270
|
+
// This prevents accessibility tooltips from showing
|
|
271
|
+
this.buttonsContainer.remove();
|
|
272
|
+
this.buttonsContainer = null;
|
|
273
|
+
this.watchCreditsButton = null;
|
|
274
|
+
this.nextEpisodeButton = null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Handle "Watch Credits" button click
|
|
279
|
+
*/
|
|
280
|
+
private handleWatchCreditsClick(): void {
|
|
281
|
+
if (!this.currentSegment) return;
|
|
282
|
+
|
|
283
|
+
this.userWatchingCredits = true;
|
|
284
|
+
this.clearTimeouts();
|
|
285
|
+
this.hideCreditsButtons('user-action');
|
|
286
|
+
|
|
287
|
+
// Emit event
|
|
288
|
+
this.callbacks.onWatchCredits(this.currentSegment);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handle "Next Episode" button click
|
|
293
|
+
*/
|
|
294
|
+
private handleNextEpisodeClick(): void {
|
|
295
|
+
if (!this.currentSegment || !this.currentSegment.nextEpisodeUrl) return;
|
|
296
|
+
|
|
297
|
+
this.clearTimeouts();
|
|
298
|
+
|
|
299
|
+
// Emit event
|
|
300
|
+
this.callbacks.onNextEpisode(this.currentSegment, this.currentSegment.nextEpisodeUrl);
|
|
301
|
+
|
|
302
|
+
// Redirect to next episode
|
|
303
|
+
this.redirectToNextEpisode(this.currentSegment.nextEpisodeUrl);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Start auto-redirect countdown
|
|
308
|
+
*/
|
|
309
|
+
private startAutoRedirectCountdown(segment: VideoSegment, delay: number): void {
|
|
310
|
+
if (!this.nextEpisodeButton || !segment.nextEpisodeUrl) return;
|
|
311
|
+
|
|
312
|
+
let remainingTime = delay;
|
|
313
|
+
const originalLabel = segment.nextEpisodeLabel || this.DEFAULT_NEXT_EPISODE_LABEL;
|
|
314
|
+
|
|
315
|
+
// Update button text with countdown
|
|
316
|
+
this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
|
|
317
|
+
|
|
318
|
+
// Create countdown progress bar
|
|
319
|
+
const progressBar = this.createProgressBar();
|
|
320
|
+
this.nextEpisodeButton.appendChild(progressBar);
|
|
321
|
+
|
|
322
|
+
// Update countdown every second
|
|
323
|
+
this.countdownInterval = setInterval(() => {
|
|
324
|
+
remainingTime -= 1;
|
|
325
|
+
|
|
326
|
+
if (this.nextEpisodeButton) {
|
|
327
|
+
this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
|
|
328
|
+
this.nextEpisodeButton.appendChild(progressBar);
|
|
329
|
+
|
|
330
|
+
// Update progress bar
|
|
331
|
+
const progress = ((delay - remainingTime) / delay) * 100;
|
|
332
|
+
progressBar.style.width = `${progress}%`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (remainingTime <= 0) {
|
|
336
|
+
this.clearTimeouts();
|
|
337
|
+
|
|
338
|
+
// Emit auto-redirect event
|
|
339
|
+
this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl!);
|
|
340
|
+
|
|
341
|
+
// Redirect to next episode
|
|
342
|
+
this.redirectToNextEpisode(segment.nextEpisodeUrl!);
|
|
343
|
+
}
|
|
344
|
+
}, 1000);
|
|
345
|
+
|
|
346
|
+
// Set final timeout as backup
|
|
347
|
+
this.autoRedirectTimeout = setTimeout(() => {
|
|
348
|
+
if (segment.nextEpisodeUrl) {
|
|
349
|
+
this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl);
|
|
350
|
+
this.redirectToNextEpisode(segment.nextEpisodeUrl);
|
|
351
|
+
}
|
|
352
|
+
}, delay * 1000);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Create countdown progress bar element
|
|
357
|
+
*/
|
|
358
|
+
private createProgressBar(): HTMLElement {
|
|
359
|
+
const progressBar = document.createElement('div');
|
|
360
|
+
progressBar.className = 'uvf-countdown-progress';
|
|
361
|
+
|
|
362
|
+
Object.assign(progressBar.style, {
|
|
363
|
+
position: 'absolute',
|
|
364
|
+
bottom: '0',
|
|
365
|
+
left: '0',
|
|
366
|
+
height: '3px',
|
|
367
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
368
|
+
width: '0%',
|
|
369
|
+
transition: 'width 1s linear',
|
|
370
|
+
borderRadius: '0 0 6px 6px'
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return progressBar;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Redirect to next episode URL
|
|
378
|
+
*/
|
|
379
|
+
private redirectToNextEpisode(url: string): void {
|
|
380
|
+
window.location.href = url;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear all timeouts and intervals
|
|
385
|
+
*/
|
|
386
|
+
private clearTimeouts(): void {
|
|
387
|
+
if (this.autoRedirectTimeout) {
|
|
388
|
+
clearTimeout(this.autoRedirectTimeout);
|
|
389
|
+
this.autoRedirectTimeout = null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.countdownInterval) {
|
|
393
|
+
clearInterval(this.countdownInterval);
|
|
394
|
+
this.countdownInterval = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|