unified-video-framework 1.4.365 → 1.4.366

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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +0 -1
  3. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  4. package/packages/core/dist/interfaces.d.ts +1 -1
  5. package/packages/core/dist/interfaces.d.ts.map +1 -1
  6. package/packages/core/src/interfaces/IVideoPlayer.ts +1 -2
  7. package/packages/core/src/interfaces.ts +4 -1
  8. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  9. package/packages/web/dist/WebPlayer.js +1 -33
  10. package/packages/web/dist/WebPlayer.js.map +1 -1
  11. package/packages/web/dist/chapters/ChapterManager.d.ts +4 -3
  12. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
  13. package/packages/web/dist/chapters/ChapterManager.js +52 -34
  14. package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
  15. package/packages/web/dist/chapters/CreditsButtonController.d.ts +44 -0
  16. package/packages/web/dist/chapters/CreditsButtonController.d.ts.map +1 -0
  17. package/packages/web/dist/chapters/CreditsButtonController.js +243 -0
  18. package/packages/web/dist/chapters/CreditsButtonController.js.map +1 -0
  19. package/packages/web/dist/chapters/SkipButtonController.d.ts +1 -4
  20. package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -1
  21. package/packages/web/dist/chapters/SkipButtonController.js +22 -118
  22. package/packages/web/dist/chapters/SkipButtonController.js.map +1 -1
  23. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +21 -12
  24. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
  25. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
  26. package/packages/web/dist/react/WebPlayerView.d.ts +0 -10
  27. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  28. package/packages/web/dist/react/WebPlayerView.js +1 -5
  29. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  30. package/packages/web/dist/react/components/SkipButton.d.ts +0 -2
  31. package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -1
  32. package/packages/web/dist/react/components/SkipButton.js +13 -30
  33. package/packages/web/dist/react/components/SkipButton.js.map +1 -1
  34. package/packages/web/src/WebPlayer.ts +7475 -7507
  35. package/packages/web/src/chapters/ChapterManager.ts +76 -57
  36. package/packages/web/src/chapters/CreditsButtonController.ts +392 -0
  37. package/packages/web/src/chapters/SkipButtonController.ts +33 -148
  38. package/packages/web/src/chapters/types/ChapterTypes.ts +34 -19
  39. package/packages/web/src/react/WebPlayerView.tsx +1 -14
  40. 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
- (segment) => this.handleSecondaryAction(segment)
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,27 @@ 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
+ this.emit('creditsFullyWatched', {
380
+ segment: this.currentSegment,
381
+ nextEpisodeUrl: this.currentSegment.nextEpisodeUrl,
382
+ currentTime
383
+ });
384
+
385
+ // Redirect to next episode after short delay
386
+ setTimeout(() => {
387
+ if (this.currentSegment?.nextEpisodeUrl) {
388
+ window.location.href = this.currentSegment.nextEpisodeUrl;
389
+ }
390
+ }, 500);
391
+ }
392
+ }
396
393
  }
397
394
 
398
395
  // Update current segment
@@ -407,8 +404,12 @@ export class ChapterManager {
407
404
  previousSegment: this.previousSegment || undefined
408
405
  });
409
406
 
410
- // Show skip button for skippable segments
411
- if (this.shouldShowSkipButton(this.currentSegment)) {
407
+ // Check if this is a credits segment with next episode URL
408
+ if (this.currentSegment.type === 'credits' && this.currentSegment.nextEpisodeUrl) {
409
+ // Show credits buttons instead of skip button
410
+ this.creditsButtonController.showCreditsButtons(this.currentSegment, currentTime);
411
+ } else if (this.shouldShowSkipButton(this.currentSegment)) {
412
+ // Show skip button for regular skippable segments
412
413
  this.skipButtonController.showSkipButton(this.currentSegment, currentTime);
413
414
  }
414
415
  }
@@ -429,17 +430,35 @@ export class ChapterManager {
429
430
  }
430
431
 
431
432
  /**
432
- * Check if we should override skip button label/behavior for Next Episode
433
+ * Handle "Watch Credits" button click
433
434
  */
434
- private checkNextEpisodeOverride(segment: VideoSegment) {
435
- if (segment.type === 'credits' && this.config.nextEpisode?.enabled) {
436
- // If we have a next episode, modifying the segment on the fly to include secondary actions
437
- // might be tricky since segment objects are shared.
438
- // Ideally, the SkipButtonController should ask for this info.
439
- // But for now, we rely on the generic SkipButton rendering secondaryLabel if passed.
440
- // The SkipButtonController needs to know to pass 'secondaryLabel' to the SkipButton.
441
- // We might need to handle this in SkipButtonController.updateSkipButton.
442
- }
435
+ private handleWatchCredits(segment: VideoSegment): void {
436
+ this.emit('creditsWatched', {
437
+ segment,
438
+ currentTime: this.videoElement.currentTime
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Handle "Next Episode" button click
444
+ */
445
+ private handleNextEpisode(segment: VideoSegment, url: string): void {
446
+ this.emit('nextEpisodeClicked', {
447
+ segment,
448
+ nextEpisodeUrl: url,
449
+ currentTime: this.videoElement.currentTime
450
+ });
451
+ }
452
+
453
+ /**
454
+ * Handle auto-redirect when countdown expires
455
+ */
456
+ private handleAutoRedirect(segment: VideoSegment, url: string): void {
457
+ this.emit('creditsAutoRedirect', {
458
+ segment,
459
+ nextEpisodeUrl: url,
460
+ currentTime: this.videoElement.currentTime
461
+ });
443
462
  }
444
463
 
445
464
  /**
@@ -0,0 +1,392 @@
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
+ this.hideButtons();
80
+ this.clearTimeouts();
81
+
82
+ // Emit event
83
+ if (this.currentSegment) {
84
+ this.callbacks.onButtonsHidden(this.currentSegment, reason);
85
+ }
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
+ this.buttonsContainer.style.opacity = '0';
270
+ }
271
+
272
+ /**
273
+ * Handle "Watch Credits" button click
274
+ */
275
+ private handleWatchCreditsClick(): void {
276
+ if (!this.currentSegment) return;
277
+
278
+ this.userWatchingCredits = true;
279
+ this.clearTimeouts();
280
+ this.hideCreditsButtons('user-action');
281
+
282
+ // Emit event
283
+ this.callbacks.onWatchCredits(this.currentSegment);
284
+ }
285
+
286
+ /**
287
+ * Handle "Next Episode" button click
288
+ */
289
+ private handleNextEpisodeClick(): void {
290
+ if (!this.currentSegment || !this.currentSegment.nextEpisodeUrl) return;
291
+
292
+ this.clearTimeouts();
293
+
294
+ // Emit event
295
+ this.callbacks.onNextEpisode(this.currentSegment, this.currentSegment.nextEpisodeUrl);
296
+
297
+ // Redirect to next episode
298
+ this.redirectToNextEpisode(this.currentSegment.nextEpisodeUrl);
299
+ }
300
+
301
+ /**
302
+ * Start auto-redirect countdown
303
+ */
304
+ private startAutoRedirectCountdown(segment: VideoSegment, delay: number): void {
305
+ if (!this.nextEpisodeButton || !segment.nextEpisodeUrl) return;
306
+
307
+ let remainingTime = delay;
308
+ const originalLabel = segment.nextEpisodeLabel || this.DEFAULT_NEXT_EPISODE_LABEL;
309
+
310
+ // Update button text with countdown
311
+ this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
312
+
313
+ // Create countdown progress bar
314
+ const progressBar = this.createProgressBar();
315
+ this.nextEpisodeButton.appendChild(progressBar);
316
+
317
+ // Update countdown every second
318
+ this.countdownInterval = setInterval(() => {
319
+ remainingTime -= 1;
320
+
321
+ if (this.nextEpisodeButton) {
322
+ this.nextEpisodeButton.textContent = `${originalLabel} (${remainingTime})`;
323
+ this.nextEpisodeButton.appendChild(progressBar);
324
+
325
+ // Update progress bar
326
+ const progress = ((delay - remainingTime) / delay) * 100;
327
+ progressBar.style.width = `${progress}%`;
328
+ }
329
+
330
+ if (remainingTime <= 0) {
331
+ this.clearTimeouts();
332
+
333
+ // Emit auto-redirect event
334
+ this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl!);
335
+
336
+ // Redirect to next episode
337
+ this.redirectToNextEpisode(segment.nextEpisodeUrl!);
338
+ }
339
+ }, 1000);
340
+
341
+ // Set final timeout as backup
342
+ this.autoRedirectTimeout = setTimeout(() => {
343
+ if (segment.nextEpisodeUrl) {
344
+ this.callbacks.onAutoRedirect(segment, segment.nextEpisodeUrl);
345
+ this.redirectToNextEpisode(segment.nextEpisodeUrl);
346
+ }
347
+ }, delay * 1000);
348
+ }
349
+
350
+ /**
351
+ * Create countdown progress bar element
352
+ */
353
+ private createProgressBar(): HTMLElement {
354
+ const progressBar = document.createElement('div');
355
+ progressBar.className = 'uvf-countdown-progress';
356
+
357
+ Object.assign(progressBar.style, {
358
+ position: 'absolute',
359
+ bottom: '0',
360
+ left: '0',
361
+ height: '3px',
362
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
363
+ width: '0%',
364
+ transition: 'width 1s linear',
365
+ borderRadius: '0 0 6px 6px'
366
+ });
367
+
368
+ return progressBar;
369
+ }
370
+
371
+ /**
372
+ * Redirect to next episode URL
373
+ */
374
+ private redirectToNextEpisode(url: string): void {
375
+ window.location.href = url;
376
+ }
377
+
378
+ /**
379
+ * Clear all timeouts and intervals
380
+ */
381
+ private clearTimeouts(): void {
382
+ if (this.autoRedirectTimeout) {
383
+ clearTimeout(this.autoRedirectTimeout);
384
+ this.autoRedirectTimeout = null;
385
+ }
386
+
387
+ if (this.countdownInterval) {
388
+ clearInterval(this.countdownInterval);
389
+ this.countdownInterval = null;
390
+ }
391
+ }
392
+ }