unified-video-framework 1.4.151 → 1.4.153

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 (93) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/chapter-manager.d.ts +39 -0
  3. package/packages/core/dist/chapter-manager.d.ts.map +1 -0
  4. package/packages/core/dist/chapter-manager.js +173 -0
  5. package/packages/core/dist/chapter-manager.js.map +1 -0
  6. package/packages/core/dist/index.d.ts +2 -0
  7. package/packages/core/dist/index.d.ts.map +1 -1
  8. package/packages/core/dist/index.js +1 -0
  9. package/packages/core/dist/index.js.map +1 -1
  10. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +10 -0
  11. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  12. package/packages/core/dist/interfaces.d.ts +33 -1
  13. package/packages/core/dist/interfaces.d.ts.map +1 -1
  14. package/packages/core/package.json +2 -2
  15. package/packages/core/src/chapter-manager.ts +290 -0
  16. package/packages/core/src/index.ts +4 -0
  17. package/packages/core/src/interfaces/IVideoPlayer.ts +11 -0
  18. package/packages/core/src/interfaces.ts +47 -1
  19. package/packages/web/dist/WebPlayer.d.ts +24 -1
  20. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  21. package/packages/web/dist/WebPlayer.js +472 -1
  22. package/packages/web/dist/WebPlayer.js.map +1 -1
  23. package/packages/web/dist/chapters/ChapterManager.d.ts +38 -0
  24. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -0
  25. package/packages/web/dist/chapters/ChapterManager.js +291 -0
  26. package/packages/web/dist/chapters/ChapterManager.js.map +1 -0
  27. package/packages/web/dist/chapters/SkipButtonController.d.ts +31 -0
  28. package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -0
  29. package/packages/web/dist/chapters/SkipButtonController.js +213 -0
  30. package/packages/web/dist/chapters/SkipButtonController.js.map +1 -0
  31. package/packages/web/dist/chapters/UserPreferencesManager.d.ts +25 -0
  32. package/packages/web/dist/chapters/UserPreferencesManager.d.ts.map +1 -0
  33. package/packages/web/dist/chapters/UserPreferencesManager.js +232 -0
  34. package/packages/web/dist/chapters/UserPreferencesManager.js.map +1 -0
  35. package/packages/web/dist/chapters/index.d.ts +12 -0
  36. package/packages/web/dist/chapters/index.d.ts.map +1 -0
  37. package/packages/web/dist/chapters/index.js +8 -0
  38. package/packages/web/dist/chapters/index.js.map +1 -0
  39. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +98 -0
  40. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -0
  41. package/packages/web/dist/chapters/types/ChapterTypes.js +31 -0
  42. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -0
  43. package/packages/web/dist/index.d.ts +1 -1
  44. package/packages/web/dist/index.d.ts.map +1 -1
  45. package/packages/web/dist/index.js +1 -1
  46. package/packages/web/dist/index.js.map +1 -1
  47. package/packages/web/dist/paywall/EmailAuthController.d.ts +1 -1
  48. package/packages/web/dist/paywall/EmailAuthController.d.ts.map +1 -1
  49. package/packages/web/dist/paywall/PaywallController.d.ts +1 -1
  50. package/packages/web/dist/paywall/PaywallController.d.ts.map +1 -1
  51. package/packages/web/dist/react/WebPlayerView.d.ts +2 -2
  52. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  53. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts +2 -2
  54. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts.map +1 -1
  55. package/packages/web/dist/react/components/ChapterProgress.d.ts +22 -0
  56. package/packages/web/dist/react/components/ChapterProgress.d.ts.map +1 -0
  57. package/packages/web/dist/react/components/ChapterProgress.js +101 -0
  58. package/packages/web/dist/react/components/ChapterProgress.js.map +1 -0
  59. package/packages/web/dist/react/components/SkipButton.d.ts +18 -0
  60. package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -0
  61. package/packages/web/dist/react/components/SkipButton.js +156 -0
  62. package/packages/web/dist/react/components/SkipButton.js.map +1 -0
  63. package/packages/web/dist/react/hooks/useChapters.d.ts +29 -0
  64. package/packages/web/dist/react/hooks/useChapters.d.ts.map +1 -0
  65. package/packages/web/dist/react/hooks/useChapters.js +158 -0
  66. package/packages/web/dist/react/hooks/useChapters.js.map +1 -0
  67. package/packages/web/package.json +3 -3
  68. package/packages/web/src/SecureVideoPlayer.ts +1 -1
  69. package/packages/web/src/WebPlayer.ts +587 -3
  70. package/packages/web/src/__tests__/WebPlayer.test.ts +1 -1
  71. package/packages/web/src/__tests__/epg-integration.test.ts +1 -1
  72. package/packages/web/src/chapters/ChapterManager.ts +464 -0
  73. package/packages/web/src/chapters/SkipButtonController.ts +353 -0
  74. package/packages/web/src/chapters/UserPreferencesManager.ts +324 -0
  75. package/packages/web/src/chapters/index.ts +34 -0
  76. package/packages/web/src/chapters/types/ChapterTypes.ts +236 -0
  77. package/packages/web/src/index.ts +1 -1
  78. package/packages/web/src/paywall/EmailAuthController.ts +1 -1
  79. package/packages/web/src/paywall/PaywallController.ts +1 -1
  80. package/packages/web/src/react/EPG.ts +1 -1
  81. package/packages/web/src/react/WebPlayerView.tsx +2 -2
  82. package/packages/web/src/react/WebPlayerViewWithEPG.tsx +3 -3
  83. package/packages/web/src/react/components/ChapterProgress.tsx +207 -0
  84. package/packages/web/src/react/components/EPGNavigationControls.tsx +1 -1
  85. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +1 -1
  86. package/packages/web/src/react/components/EPGOverlay.tsx +1 -1
  87. package/packages/web/src/react/components/EPGProgramGrid.tsx +1 -1
  88. package/packages/web/src/react/components/EPGTimelineHeader.tsx +1 -1
  89. package/packages/web/src/react/components/SkipButton.tsx +278 -0
  90. package/packages/web/src/react/hooks/useChapters.ts +308 -0
  91. package/packages/web/src/react/types/EPGTypes.ts +1 -1
  92. package/packages/web/src/react/utils/EPGUtils.ts +1 -1
  93. package/packages/web/src/test/epg-test.ts +1 -1
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Controller for skip button UI and interactions
3
+ */
4
+
5
+ import {
6
+ VideoSegment,
7
+ SkipButtonState,
8
+ SkipButtonPosition,
9
+ ChapterConfig,
10
+ DEFAULT_SKIP_LABELS
11
+ } from './types/ChapterTypes';
12
+
13
+ export class SkipButtonController {
14
+ private skipButton: HTMLElement | null = null;
15
+ private currentSegment: VideoSegment | null = null;
16
+ private autoSkipTimeout: NodeJS.Timeout | null = null;
17
+ private hideTimeout: NodeJS.Timeout | null = null;
18
+ private countdownInterval: NodeJS.Timeout | null = null;
19
+ private state: SkipButtonState;
20
+
21
+ constructor(
22
+ private playerContainer: HTMLElement,
23
+ private config: ChapterConfig,
24
+ private onSkip: (segment: VideoSegment) => void,
25
+ private onButtonShown: (segment: VideoSegment) => void,
26
+ private onButtonHidden: (segment: VideoSegment, reason: string) => void
27
+ ) {
28
+ this.state = {
29
+ visible: false,
30
+ segment: null,
31
+ position: config.skipButtonPosition || 'bottom-right'
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Show skip button for a segment
37
+ */
38
+ public showSkipButton(segment: VideoSegment, currentTime: number): void {
39
+ // Check if skip buttons are disabled in preferences
40
+ if (!this.config.userPreferences?.showSkipButtons) {
41
+ return;
42
+ }
43
+
44
+ // Check if this specific segment should show a skip button
45
+ if (segment.showSkipButton === false) {
46
+ return;
47
+ }
48
+
49
+ this.currentSegment = segment;
50
+ this.state.segment = segment;
51
+
52
+ // Create button if it doesn't exist
53
+ if (!this.skipButton) {
54
+ this.skipButton = this.createSkipButton();
55
+ this.playerContainer.appendChild(this.skipButton);
56
+ }
57
+
58
+ // Update button content and show it
59
+ this.updateSkipButton(segment);
60
+ this.showButton();
61
+
62
+ // Handle auto-skip functionality
63
+ this.handleAutoSkip(segment, currentTime);
64
+
65
+ // Handle auto-hide functionality
66
+ this.handleAutoHide();
67
+
68
+ // Emit event
69
+ this.onButtonShown(segment);
70
+ }
71
+
72
+ /**
73
+ * Hide skip button
74
+ */
75
+ public hideSkipButton(reason: 'timeout' | 'segment-end' | 'user-action' | 'manual' = 'manual'): void {
76
+ if (!this.skipButton || !this.state.visible) {
77
+ return;
78
+ }
79
+
80
+ this.hideButton();
81
+ this.clearTimeouts();
82
+
83
+ // Emit event
84
+ if (this.currentSegment) {
85
+ this.onButtonHidden(this.currentSegment, reason);
86
+ }
87
+
88
+ this.state.visible = false;
89
+ this.state.segment = null;
90
+ this.currentSegment = null;
91
+ }
92
+
93
+ /**
94
+ * Update skip button position
95
+ */
96
+ public updatePosition(position: SkipButtonPosition): void {
97
+ this.state.position = position;
98
+ if (this.skipButton) {
99
+ this.applyPositionStyles(this.skipButton, position);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Check if button is currently visible
105
+ */
106
+ public isVisible(): boolean {
107
+ return this.state.visible;
108
+ }
109
+
110
+ /**
111
+ * Get current button state
112
+ */
113
+ public getState(): SkipButtonState {
114
+ return { ...this.state };
115
+ }
116
+
117
+ /**
118
+ * Destroy the skip button controller
119
+ */
120
+ public destroy(): void {
121
+ this.clearTimeouts();
122
+ if (this.skipButton) {
123
+ this.skipButton.remove();
124
+ this.skipButton = null;
125
+ }
126
+ this.currentSegment = null;
127
+ this.state.visible = false;
128
+ this.state.segment = null;
129
+ }
130
+
131
+ /**
132
+ * Create the skip button DOM element
133
+ */
134
+ private createSkipButton(): HTMLElement {
135
+ const button = document.createElement('button');
136
+ button.className = 'uvf-skip-button';
137
+ button.setAttribute('type', 'button');
138
+ button.setAttribute('aria-label', 'Skip segment');
139
+
140
+ // Apply position styles
141
+ this.applyPositionStyles(button, this.state.position);
142
+
143
+ // Add click handler
144
+ button.addEventListener('click', () => {
145
+ if (this.currentSegment) {
146
+ this.onSkip(this.currentSegment);
147
+ this.hideSkipButton('user-action');
148
+ }
149
+ });
150
+
151
+ // Apply custom styles if provided
152
+ if (this.config.customStyles?.skipButton) {
153
+ Object.assign(button.style, this.config.customStyles.skipButton);
154
+ }
155
+
156
+ return button;
157
+ }
158
+
159
+ /**
160
+ * Update skip button content for current segment
161
+ */
162
+ private updateSkipButton(segment: VideoSegment): void {
163
+ if (!this.skipButton) return;
164
+
165
+ // Set button text
166
+ const skipLabel = segment.skipLabel || DEFAULT_SKIP_LABELS[segment.type];
167
+ this.skipButton.textContent = skipLabel;
168
+
169
+ // Update aria-label for accessibility
170
+ this.skipButton.setAttribute('aria-label', `${skipLabel} - ${segment.title || segment.type}`);
171
+
172
+ // Add segment type class for styling
173
+ this.skipButton.className = `uvf-skip-button uvf-skip-${segment.type}`;
174
+
175
+ // Apply position class
176
+ this.skipButton.classList.add(`uvf-skip-button-${this.state.position}`);
177
+ }
178
+
179
+ /**
180
+ * Apply position styles to skip button
181
+ */
182
+ private applyPositionStyles(button: HTMLElement, position: SkipButtonPosition): void {
183
+ // Reset position classes
184
+ button.classList.remove(
185
+ 'uvf-skip-button-bottom-right',
186
+ 'uvf-skip-button-bottom-left',
187
+ 'uvf-skip-button-top-right',
188
+ 'uvf-skip-button-top-left'
189
+ );
190
+
191
+ // Add new position class
192
+ button.classList.add(`uvf-skip-button-${position}`);
193
+
194
+ // Apply CSS styles based on position
195
+ const styles: Partial<CSSStyleDeclaration> = {
196
+ position: 'absolute',
197
+ zIndex: '1000'
198
+ };
199
+
200
+ switch (position) {
201
+ case 'bottom-right':
202
+ Object.assign(styles, {
203
+ bottom: '100px',
204
+ right: '30px'
205
+ });
206
+ break;
207
+ case 'bottom-left':
208
+ Object.assign(styles, {
209
+ bottom: '100px',
210
+ left: '30px'
211
+ });
212
+ break;
213
+ case 'top-right':
214
+ Object.assign(styles, {
215
+ top: '30px',
216
+ right: '30px'
217
+ });
218
+ break;
219
+ case 'top-left':
220
+ Object.assign(styles, {
221
+ top: '30px',
222
+ left: '30px'
223
+ });
224
+ break;
225
+ }
226
+
227
+ Object.assign(button.style, styles);
228
+ }
229
+
230
+ /**
231
+ * Show the skip button with animation
232
+ */
233
+ private showButton(): void {
234
+ if (!this.skipButton) return;
235
+
236
+ this.skipButton.classList.add('visible');
237
+ this.state.visible = true;
238
+ }
239
+
240
+ /**
241
+ * Hide the skip button with animation
242
+ */
243
+ private hideButton(): void {
244
+ if (!this.skipButton) return;
245
+
246
+ this.skipButton.classList.remove('visible');
247
+ this.skipButton.classList.remove('auto-skip', 'countdown');
248
+ }
249
+
250
+ /**
251
+ * Handle auto-skip functionality
252
+ */
253
+ private handleAutoSkip(segment: VideoSegment, currentTime: number): void {
254
+ if (!segment.autoSkip || !segment.autoSkipDelay) {
255
+ return;
256
+ }
257
+
258
+ // Check user preferences for auto-skip
259
+ const preferences = this.config.userPreferences;
260
+ const shouldAutoSkip = (
261
+ (segment.type === 'intro' && preferences?.autoSkipIntro) ||
262
+ (segment.type === 'recap' && preferences?.autoSkipRecap) ||
263
+ (segment.type === 'credits' && preferences?.autoSkipCredits)
264
+ );
265
+
266
+ if (!shouldAutoSkip) {
267
+ return;
268
+ }
269
+
270
+ // Add auto-skip class for styling
271
+ this.skipButton?.classList.add('auto-skip');
272
+
273
+ // Start countdown
274
+ this.startAutoSkipCountdown(segment, segment.autoSkipDelay);
275
+ }
276
+
277
+ /**
278
+ * Start auto-skip countdown
279
+ */
280
+ private startAutoSkipCountdown(segment: VideoSegment, delay: number): void {
281
+ if (!this.skipButton) return;
282
+
283
+ let remainingTime = delay;
284
+ this.state.autoSkipCountdown = remainingTime;
285
+
286
+ // Update button text with countdown
287
+ const originalText = this.skipButton.textContent || '';
288
+
289
+ // Start countdown animation
290
+ this.skipButton.classList.add('countdown');
291
+
292
+ // Update countdown every second
293
+ this.countdownInterval = setInterval(() => {
294
+ remainingTime -= 1;
295
+ this.state.autoSkipCountdown = remainingTime;
296
+
297
+ if (this.skipButton) {
298
+ this.skipButton.textContent = `${originalText} (${remainingTime})`;
299
+ }
300
+
301
+ if (remainingTime <= 0) {
302
+ this.clearTimeouts();
303
+ if (this.currentSegment) {
304
+ this.onSkip(this.currentSegment);
305
+ this.hideSkipButton('timeout');
306
+ }
307
+ }
308
+ }, 1000);
309
+
310
+ // Set final timeout as backup
311
+ this.autoSkipTimeout = setTimeout(() => {
312
+ if (this.currentSegment) {
313
+ this.onSkip(this.currentSegment);
314
+ this.hideSkipButton('timeout');
315
+ }
316
+ }, delay * 1000);
317
+ }
318
+
319
+ /**
320
+ * Handle auto-hide functionality
321
+ */
322
+ private handleAutoHide(): void {
323
+ if (!this.config.autoHide || !this.config.autoHideDelay) {
324
+ return;
325
+ }
326
+
327
+ this.hideTimeout = setTimeout(() => {
328
+ this.hideSkipButton('timeout');
329
+ }, this.config.autoHideDelay);
330
+ }
331
+
332
+ /**
333
+ * Clear all timeouts
334
+ */
335
+ private clearTimeouts(): void {
336
+ if (this.autoSkipTimeout) {
337
+ clearTimeout(this.autoSkipTimeout);
338
+ this.autoSkipTimeout = null;
339
+ }
340
+
341
+ if (this.hideTimeout) {
342
+ clearTimeout(this.hideTimeout);
343
+ this.hideTimeout = null;
344
+ }
345
+
346
+ if (this.countdownInterval) {
347
+ clearInterval(this.countdownInterval);
348
+ this.countdownInterval = null;
349
+ }
350
+
351
+ this.state.autoSkipCountdown = undefined;
352
+ }
353
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * User preferences manager for chapter and skip functionality
3
+ */
4
+
5
+ import { ChapterPreferences } from './types/ChapterTypes';
6
+
7
+ export class UserPreferencesManager {
8
+ private static readonly STORAGE_KEY = 'uvf_chapter_preferences';
9
+ private static readonly DEFAULT_PREFERENCES: ChapterPreferences = {
10
+ autoSkipIntro: false,
11
+ autoSkipRecap: false,
12
+ autoSkipCredits: false,
13
+ showSkipButtons: true,
14
+ skipButtonTimeout: 5000,
15
+ rememberChoices: true
16
+ };
17
+
18
+ private preferences: ChapterPreferences;
19
+ private listeners: ((preferences: ChapterPreferences) => void)[] = [];
20
+
21
+ constructor(initialPreferences?: Partial<ChapterPreferences>) {
22
+ // Load preferences from storage or use defaults
23
+ this.preferences = this.loadPreferences();
24
+
25
+ // Apply initial preferences if provided
26
+ if (initialPreferences) {
27
+ this.updatePreferences(initialPreferences);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Get current preferences
33
+ */
34
+ public getPreferences(): ChapterPreferences {
35
+ return { ...this.preferences };
36
+ }
37
+
38
+ /**
39
+ * Update preferences
40
+ */
41
+ public updatePreferences(updates: Partial<ChapterPreferences>): void {
42
+ const oldPreferences = { ...this.preferences };
43
+ this.preferences = { ...this.preferences, ...updates };
44
+
45
+ // Save to storage if rememberChoices is enabled
46
+ if (this.preferences.rememberChoices) {
47
+ this.savePreferences();
48
+ }
49
+
50
+ // Notify listeners if preferences changed
51
+ if (!this.preferencesEqual(oldPreferences, this.preferences)) {
52
+ this.notifyListeners();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Reset preferences to defaults
58
+ */
59
+ public resetPreferences(): void {
60
+ this.preferences = { ...UserPreferencesManager.DEFAULT_PREFERENCES };
61
+ this.savePreferences();
62
+ this.notifyListeners();
63
+ }
64
+
65
+ /**
66
+ * Get specific preference value
67
+ */
68
+ public getPreference<K extends keyof ChapterPreferences>(key: K): ChapterPreferences[K] {
69
+ return this.preferences[key];
70
+ }
71
+
72
+ /**
73
+ * Set specific preference value
74
+ */
75
+ public setPreference<K extends keyof ChapterPreferences>(
76
+ key: K,
77
+ value: ChapterPreferences[K]
78
+ ): void {
79
+ this.updatePreferences({ [key]: value } as Partial<ChapterPreferences>);
80
+ }
81
+
82
+ /**
83
+ * Toggle auto-skip for specific segment type
84
+ */
85
+ public toggleAutoSkip(segmentType: 'intro' | 'recap' | 'credits'): void {
86
+ switch (segmentType) {
87
+ case 'intro':
88
+ this.setPreference('autoSkipIntro', !this.preferences.autoSkipIntro);
89
+ break;
90
+ case 'recap':
91
+ this.setPreference('autoSkipRecap', !this.preferences.autoSkipRecap);
92
+ break;
93
+ case 'credits':
94
+ this.setPreference('autoSkipCredits', !this.preferences.autoSkipCredits);
95
+ break;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Check if auto-skip is enabled for segment type
101
+ */
102
+ public isAutoSkipEnabled(segmentType: 'intro' | 'recap' | 'credits'): boolean {
103
+ switch (segmentType) {
104
+ case 'intro':
105
+ return this.preferences.autoSkipIntro || false;
106
+ case 'recap':
107
+ return this.preferences.autoSkipRecap || false;
108
+ case 'credits':
109
+ return this.preferences.autoSkipCredits || false;
110
+ default:
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Add preference change listener
117
+ */
118
+ public addListener(listener: (preferences: ChapterPreferences) => void): void {
119
+ this.listeners.push(listener);
120
+ }
121
+
122
+ /**
123
+ * Remove preference change listener
124
+ */
125
+ public removeListener(listener: (preferences: ChapterPreferences) => void): void {
126
+ const index = this.listeners.indexOf(listener);
127
+ if (index > -1) {
128
+ this.listeners.splice(index, 1);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Create preferences UI panel
134
+ */
135
+ public createPreferencesPanel(): HTMLElement {
136
+ const panel = document.createElement('div');
137
+ panel.className = 'uvf-chapter-preferences-panel';
138
+ panel.innerHTML = `
139
+ <div class="uvf-preferences-header">
140
+ <h3>Skip Preferences</h3>
141
+ </div>
142
+ <div class="uvf-preferences-body">
143
+ <div class="uvf-preference-item">
144
+ <label>
145
+ <input type="checkbox" id="uvf-pref-auto-skip-intro" ${this.preferences.autoSkipIntro ? 'checked' : ''}>
146
+ <span>Auto-skip intros</span>
147
+ </label>
148
+ </div>
149
+ <div class="uvf-preference-item">
150
+ <label>
151
+ <input type="checkbox" id="uvf-pref-auto-skip-recap" ${this.preferences.autoSkipRecap ? 'checked' : ''}>
152
+ <span>Auto-skip recaps</span>
153
+ </label>
154
+ </div>
155
+ <div class="uvf-preference-item">
156
+ <label>
157
+ <input type="checkbox" id="uvf-pref-auto-skip-credits" ${this.preferences.autoSkipCredits ? 'checked' : ''}>
158
+ <span>Auto-skip credits</span>
159
+ </label>
160
+ </div>
161
+ <div class="uvf-preference-item">
162
+ <label>
163
+ <input type="checkbox" id="uvf-pref-show-buttons" ${this.preferences.showSkipButtons ? 'checked' : ''}>
164
+ <span>Show skip buttons</span>
165
+ </label>
166
+ </div>
167
+ <div class="uvf-preference-item">
168
+ <label>
169
+ <span>Button timeout:</span>
170
+ <select id="uvf-pref-timeout">
171
+ <option value="3000" ${this.preferences.skipButtonTimeout === 3000 ? 'selected' : ''}>3 seconds</option>
172
+ <option value="5000" ${this.preferences.skipButtonTimeout === 5000 ? 'selected' : ''}>5 seconds</option>
173
+ <option value="10000" ${this.preferences.skipButtonTimeout === 10000 ? 'selected' : ''}>10 seconds</option>
174
+ <option value="0" ${this.preferences.skipButtonTimeout === 0 ? 'selected' : ''}>Never hide</option>
175
+ </select>
176
+ </label>
177
+ </div>
178
+ <div class="uvf-preference-item">
179
+ <label>
180
+ <input type="checkbox" id="uvf-pref-remember" ${this.preferences.rememberChoices ? 'checked' : ''}>
181
+ <span>Remember preferences</span>
182
+ </label>
183
+ </div>
184
+ </div>
185
+ <div class="uvf-preferences-footer">
186
+ <button type="button" id="uvf-pref-reset">Reset to Defaults</button>
187
+ </div>
188
+ `;
189
+
190
+ // Add event listeners
191
+ this.setupPreferencesEventListeners(panel);
192
+
193
+ return panel;
194
+ }
195
+
196
+ /**
197
+ * Load preferences from localStorage
198
+ */
199
+ private loadPreferences(): ChapterPreferences {
200
+ try {
201
+ const stored = localStorage.getItem(UserPreferencesManager.STORAGE_KEY);
202
+ if (stored) {
203
+ const parsed = JSON.parse(stored);
204
+ return { ...UserPreferencesManager.DEFAULT_PREFERENCES, ...parsed };
205
+ }
206
+ } catch (error) {
207
+ console.warn('Failed to load chapter preferences from storage:', error);
208
+ }
209
+
210
+ return { ...UserPreferencesManager.DEFAULT_PREFERENCES };
211
+ }
212
+
213
+ /**
214
+ * Save preferences to localStorage
215
+ */
216
+ private savePreferences(): void {
217
+ try {
218
+ localStorage.setItem(
219
+ UserPreferencesManager.STORAGE_KEY,
220
+ JSON.stringify(this.preferences)
221
+ );
222
+ } catch (error) {
223
+ console.warn('Failed to save chapter preferences to storage:', error);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Check if two preference objects are equal
229
+ */
230
+ private preferencesEqual(a: ChapterPreferences, b: ChapterPreferences): boolean {
231
+ return (
232
+ a.autoSkipIntro === b.autoSkipIntro &&
233
+ a.autoSkipRecap === b.autoSkipRecap &&
234
+ a.autoSkipCredits === b.autoSkipCredits &&
235
+ a.showSkipButtons === b.showSkipButtons &&
236
+ a.skipButtonTimeout === b.skipButtonTimeout &&
237
+ a.rememberChoices === b.rememberChoices
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Notify all listeners of preference changes
243
+ */
244
+ private notifyListeners(): void {
245
+ this.listeners.forEach(listener => {
246
+ try {
247
+ listener(this.getPreferences());
248
+ } catch (error) {
249
+ console.error('Error in preference change listener:', error);
250
+ }
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Setup event listeners for preferences panel
256
+ */
257
+ private setupPreferencesEventListeners(panel: HTMLElement): void {
258
+ // Auto-skip checkboxes
259
+ const autoSkipIntro = panel.querySelector('#uvf-pref-auto-skip-intro') as HTMLInputElement;
260
+ const autoSkipRecap = panel.querySelector('#uvf-pref-auto-skip-recap') as HTMLInputElement;
261
+ const autoSkipCredits = panel.querySelector('#uvf-pref-auto-skip-credits') as HTMLInputElement;
262
+ const showButtons = panel.querySelector('#uvf-pref-show-buttons') as HTMLInputElement;
263
+ const remember = panel.querySelector('#uvf-pref-remember') as HTMLInputElement;
264
+ const timeout = panel.querySelector('#uvf-pref-timeout') as HTMLSelectElement;
265
+ const resetButton = panel.querySelector('#uvf-pref-reset') as HTMLButtonElement;
266
+
267
+ // Event listeners
268
+ if (autoSkipIntro) {
269
+ autoSkipIntro.addEventListener('change', () => {
270
+ this.setPreference('autoSkipIntro', autoSkipIntro.checked);
271
+ });
272
+ }
273
+
274
+ if (autoSkipRecap) {
275
+ autoSkipRecap.addEventListener('change', () => {
276
+ this.setPreference('autoSkipRecap', autoSkipRecap.checked);
277
+ });
278
+ }
279
+
280
+ if (autoSkipCredits) {
281
+ autoSkipCredits.addEventListener('change', () => {
282
+ this.setPreference('autoSkipCredits', autoSkipCredits.checked);
283
+ });
284
+ }
285
+
286
+ if (showButtons) {
287
+ showButtons.addEventListener('change', () => {
288
+ this.setPreference('showSkipButtons', showButtons.checked);
289
+ });
290
+ }
291
+
292
+ if (remember) {
293
+ remember.addEventListener('change', () => {
294
+ this.setPreference('rememberChoices', remember.checked);
295
+ });
296
+ }
297
+
298
+ if (timeout) {
299
+ timeout.addEventListener('change', () => {
300
+ this.setPreference('skipButtonTimeout', parseInt(timeout.value, 10));
301
+ });
302
+ }
303
+
304
+ if (resetButton) {
305
+ resetButton.addEventListener('click', () => {
306
+ this.resetPreferences();
307
+ // Update UI
308
+ if (autoSkipIntro) autoSkipIntro.checked = this.preferences.autoSkipIntro || false;
309
+ if (autoSkipRecap) autoSkipRecap.checked = this.preferences.autoSkipRecap || false;
310
+ if (autoSkipCredits) autoSkipCredits.checked = this.preferences.autoSkipCredits || false;
311
+ if (showButtons) showButtons.checked = this.preferences.showSkipButtons || true;
312
+ if (remember) remember.checked = this.preferences.rememberChoices || true;
313
+ if (timeout) timeout.value = String(this.preferences.skipButtonTimeout || 5000);
314
+ });
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Get default preferences
320
+ */
321
+ public static getDefaultPreferences(): ChapterPreferences {
322
+ return { ...UserPreferencesManager.DEFAULT_PREFERENCES };
323
+ }
324
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Chapter functionality exports for unified-video-framework
3
+ */
4
+
5
+ // Core classes
6
+ export { ChapterManager } from './ChapterManager';
7
+ export { SkipButtonController } from './SkipButtonController';
8
+ export { UserPreferencesManager } from './UserPreferencesManager';
9
+
10
+ // Types and interfaces
11
+ export * from './types/ChapterTypes';
12
+
13
+ // React components and hooks
14
+ export { useChapters } from '../react/hooks/useChapters';
15
+ export { SkipButton } from '../react/components/SkipButton';
16
+ export { ChapterProgress } from '../react/components/ChapterProgress';
17
+
18
+ // Re-export commonly used types
19
+ export type {
20
+ UseChaptersOptions,
21
+ UseChaptersResult
22
+ } from '../react/hooks/useChapters';
23
+
24
+ export type {
25
+ SkipButtonProps
26
+ } from '../react/components/SkipButton';
27
+
28
+ export type {
29
+ ChapterProgressProps
30
+ } from '../react/components/ChapterProgress';
31
+
32
+ export type {
33
+ ChapterMarker
34
+ } from '../react/components/ChapterProgress';