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.
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 +51 -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 +246 -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 +77 -57
  36. package/packages/web/src/chapters/CreditsButtonController.ts +397 -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,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
- // Show skip button for skippable segments
411
- if (this.shouldShowSkipButton(this.currentSegment)) {
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
- * Check if we should override skip button label/behavior for Next Episode
434
+ * Handle "Watch Credits" button click
433
435
  */
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
- }
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
+ }