unified-video-framework 1.4.151 → 1.4.154
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/chapter-manager.d.ts +39 -0
- package/packages/core/dist/chapter-manager.d.ts.map +1 -0
- package/packages/core/dist/chapter-manager.js +173 -0
- package/packages/core/dist/chapter-manager.js.map +1 -0
- package/packages/core/dist/index.d.ts +2 -0
- package/packages/core/dist/index.d.ts.map +1 -1
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +10 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/dist/interfaces.d.ts +33 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/chapter-manager.ts +290 -0
- package/packages/core/src/index.ts +4 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +11 -0
- package/packages/core/src/interfaces.ts +47 -1
- package/packages/web/dist/WebPlayer.d.ts +24 -1
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +472 -1
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts +38 -0
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -0
- package/packages/web/dist/chapters/ChapterManager.js +291 -0
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -0
- package/packages/web/dist/chapters/SkipButtonController.d.ts +31 -0
- package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -0
- package/packages/web/dist/chapters/SkipButtonController.js +213 -0
- package/packages/web/dist/chapters/SkipButtonController.js.map +1 -0
- package/packages/web/dist/chapters/UserPreferencesManager.d.ts +25 -0
- package/packages/web/dist/chapters/UserPreferencesManager.d.ts.map +1 -0
- package/packages/web/dist/chapters/UserPreferencesManager.js +232 -0
- package/packages/web/dist/chapters/UserPreferencesManager.js.map +1 -0
- package/packages/web/dist/chapters/index.d.ts +12 -0
- package/packages/web/dist/chapters/index.d.ts.map +1 -0
- package/packages/web/dist/chapters/index.js +8 -0
- package/packages/web/dist/chapters/index.js.map +1 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +98 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -0
- package/packages/web/dist/chapters/types/ChapterTypes.js +31 -0
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -0
- package/packages/web/dist/index.d.ts +1 -1
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +1 -1
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +2 -2
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts +2 -2
- package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts.map +1 -1
- package/packages/web/dist/react/components/ChapterProgress.d.ts +22 -0
- package/packages/web/dist/react/components/ChapterProgress.d.ts.map +1 -0
- package/packages/web/dist/react/components/ChapterProgress.js +101 -0
- package/packages/web/dist/react/components/ChapterProgress.js.map +1 -0
- package/packages/web/dist/react/components/SkipButton.d.ts +18 -0
- package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -0
- package/packages/web/dist/react/components/SkipButton.js +156 -0
- package/packages/web/dist/react/components/SkipButton.js.map +1 -0
- package/packages/web/dist/react/hooks/useChapters.d.ts +29 -0
- package/packages/web/dist/react/hooks/useChapters.d.ts.map +1 -0
- package/packages/web/dist/react/hooks/useChapters.js +158 -0
- package/packages/web/dist/react/hooks/useChapters.js.map +1 -0
- package/packages/web/package.json +0 -3
- package/packages/web/src/SecureVideoPlayer.ts +1 -1
- package/packages/web/src/WebPlayer.ts +587 -3
- package/packages/web/src/__tests__/WebPlayer.test.ts +1 -1
- package/packages/web/src/__tests__/epg-integration.test.ts +1 -1
- package/packages/web/src/chapters/ChapterManager.ts +464 -0
- package/packages/web/src/chapters/SkipButtonController.ts +353 -0
- package/packages/web/src/chapters/UserPreferencesManager.ts +324 -0
- package/packages/web/src/chapters/index.ts +34 -0
- package/packages/web/src/chapters/types/ChapterTypes.ts +236 -0
- package/packages/web/src/index.ts +1 -1
- package/packages/web/src/react/EPG.ts +1 -1
- package/packages/web/src/react/WebPlayerView.tsx +2 -2
- package/packages/web/src/react/WebPlayerViewWithEPG.tsx +3 -3
- package/packages/web/src/react/components/ChapterProgress.tsx +207 -0
- package/packages/web/src/react/components/EPGNavigationControls.tsx +1 -1
- package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +1 -1
- package/packages/web/src/react/components/EPGOverlay.tsx +1 -1
- package/packages/web/src/react/components/EPGProgramGrid.tsx +1 -1
- package/packages/web/src/react/components/EPGTimelineHeader.tsx +1 -1
- package/packages/web/src/react/components/SkipButton.tsx +278 -0
- package/packages/web/src/react/hooks/useChapters.ts +308 -0
- package/packages/web/src/react/types/EPGTypes.ts +1 -1
- package/packages/web/src/react/utils/EPGUtils.ts +1 -1
- 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';
|