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.
- 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/package.json +2 -2
- 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/paywall/EmailAuthController.d.ts +1 -1
- package/packages/web/dist/paywall/EmailAuthController.d.ts.map +1 -1
- package/packages/web/dist/paywall/PaywallController.d.ts +1 -1
- package/packages/web/dist/paywall/PaywallController.d.ts.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 +3 -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/paywall/EmailAuthController.ts +1 -1
- package/packages/web/src/paywall/PaywallController.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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and interfaces for video chapters and skip functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SegmentType = 'intro' | 'recap' | 'content' | 'credits' | 'ad';
|
|
6
|
+
|
|
7
|
+
export type SkipButtonPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a video segment/chapter
|
|
11
|
+
*/
|
|
12
|
+
export interface VideoSegment {
|
|
13
|
+
/** Unique identifier for the segment */
|
|
14
|
+
id: string;
|
|
15
|
+
|
|
16
|
+
/** Type of segment */
|
|
17
|
+
type: SegmentType;
|
|
18
|
+
|
|
19
|
+
/** Start time in seconds */
|
|
20
|
+
startTime: number;
|
|
21
|
+
|
|
22
|
+
/** End time in seconds */
|
|
23
|
+
endTime: number;
|
|
24
|
+
|
|
25
|
+
/** Display title for the segment */
|
|
26
|
+
title?: string;
|
|
27
|
+
|
|
28
|
+
/** Description of the segment */
|
|
29
|
+
description?: string;
|
|
30
|
+
|
|
31
|
+
/** Custom text for the skip button */
|
|
32
|
+
skipLabel?: string;
|
|
33
|
+
|
|
34
|
+
/** Whether to auto-skip after a delay */
|
|
35
|
+
autoSkip?: boolean;
|
|
36
|
+
|
|
37
|
+
/** Delay in seconds before auto-skip */
|
|
38
|
+
autoSkipDelay?: number;
|
|
39
|
+
|
|
40
|
+
/** Whether to show skip button for this segment */
|
|
41
|
+
showSkipButton?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Complete chapter data for a video
|
|
46
|
+
*/
|
|
47
|
+
export interface VideoChapters {
|
|
48
|
+
/** Video identifier */
|
|
49
|
+
videoId: string;
|
|
50
|
+
|
|
51
|
+
/** Total video duration in seconds */
|
|
52
|
+
duration: number;
|
|
53
|
+
|
|
54
|
+
/** Array of video segments */
|
|
55
|
+
segments: VideoSegment[];
|
|
56
|
+
|
|
57
|
+
/** Metadata about the chapters */
|
|
58
|
+
metadata?: {
|
|
59
|
+
version?: string;
|
|
60
|
+
createdAt?: string;
|
|
61
|
+
updatedAt?: string;
|
|
62
|
+
source?: 'manual' | 'auto-detected' | 'crowdsourced';
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Configuration for chapter functionality
|
|
68
|
+
*/
|
|
69
|
+
export interface ChapterConfig {
|
|
70
|
+
/** Enable/disable chapter functionality */
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
|
|
73
|
+
/** Chapter data object */
|
|
74
|
+
data?: VideoChapters;
|
|
75
|
+
|
|
76
|
+
/** URL to fetch chapter data from */
|
|
77
|
+
dataUrl?: string;
|
|
78
|
+
|
|
79
|
+
/** Auto-hide skip button after showing */
|
|
80
|
+
autoHide?: boolean;
|
|
81
|
+
|
|
82
|
+
/** Delay before auto-hiding skip button (ms) */
|
|
83
|
+
autoHideDelay?: number;
|
|
84
|
+
|
|
85
|
+
/** Show chapter markers on progress bar */
|
|
86
|
+
showChapterMarkers?: boolean;
|
|
87
|
+
|
|
88
|
+
/** Position of skip button */
|
|
89
|
+
skipButtonPosition?: SkipButtonPosition;
|
|
90
|
+
|
|
91
|
+
/** Custom CSS styles */
|
|
92
|
+
customStyles?: {
|
|
93
|
+
skipButton?: Partial<CSSStyleDeclaration>;
|
|
94
|
+
chapterMarkers?: Partial<CSSStyleDeclaration>;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** User preferences */
|
|
98
|
+
userPreferences?: ChapterPreferences;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* User preferences for chapter behavior
|
|
103
|
+
*/
|
|
104
|
+
export interface ChapterPreferences {
|
|
105
|
+
/** Auto-skip intro segments */
|
|
106
|
+
autoSkipIntro?: boolean;
|
|
107
|
+
|
|
108
|
+
/** Auto-skip recap segments */
|
|
109
|
+
autoSkipRecap?: boolean;
|
|
110
|
+
|
|
111
|
+
/** Auto-skip credits segments */
|
|
112
|
+
autoSkipCredits?: boolean;
|
|
113
|
+
|
|
114
|
+
/** Show skip buttons */
|
|
115
|
+
showSkipButtons?: boolean;
|
|
116
|
+
|
|
117
|
+
/** Skip button timeout in milliseconds */
|
|
118
|
+
skipButtonTimeout?: number;
|
|
119
|
+
|
|
120
|
+
/** Remember user choices */
|
|
121
|
+
rememberChoices?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Events emitted by the chapter system
|
|
126
|
+
*/
|
|
127
|
+
export interface ChapterEvents {
|
|
128
|
+
/** When entering a new segment */
|
|
129
|
+
segmentEntered: {
|
|
130
|
+
segment: VideoSegment;
|
|
131
|
+
currentTime: number;
|
|
132
|
+
previousSegment?: VideoSegment;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** When exiting a segment */
|
|
136
|
+
segmentExited: {
|
|
137
|
+
segment: VideoSegment;
|
|
138
|
+
currentTime: number;
|
|
139
|
+
nextSegment?: VideoSegment;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** When a segment is skipped */
|
|
143
|
+
segmentSkipped: {
|
|
144
|
+
fromSegment: VideoSegment;
|
|
145
|
+
toSegment?: VideoSegment;
|
|
146
|
+
skipMethod: 'button' | 'auto' | 'manual';
|
|
147
|
+
currentTime: number;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** When skip button is shown */
|
|
151
|
+
skipButtonShown: {
|
|
152
|
+
segment: VideoSegment;
|
|
153
|
+
currentTime: number;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** When skip button is hidden */
|
|
157
|
+
skipButtonHidden: {
|
|
158
|
+
segment: VideoSegment;
|
|
159
|
+
currentTime: number;
|
|
160
|
+
reason: 'timeout' | 'segment-end' | 'user-action' | 'manual';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** When chapters are loaded */
|
|
164
|
+
chaptersLoaded: {
|
|
165
|
+
chapters: VideoChapters;
|
|
166
|
+
segmentCount: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** When chapter loading fails */
|
|
170
|
+
chaptersLoadError: {
|
|
171
|
+
error: Error;
|
|
172
|
+
url?: string;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Skip button state
|
|
178
|
+
*/
|
|
179
|
+
export interface SkipButtonState {
|
|
180
|
+
visible: boolean;
|
|
181
|
+
segment: VideoSegment | null;
|
|
182
|
+
autoSkipCountdown?: number;
|
|
183
|
+
position: SkipButtonPosition;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Chapter marker data for progress bar
|
|
188
|
+
*/
|
|
189
|
+
export interface ChapterMarker {
|
|
190
|
+
segment: VideoSegment;
|
|
191
|
+
position: number; // Percentage position on progress bar (0-100)
|
|
192
|
+
color?: string;
|
|
193
|
+
label?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Default configuration values
|
|
198
|
+
*/
|
|
199
|
+
export const DEFAULT_CHAPTER_CONFIG: ChapterConfig = {
|
|
200
|
+
enabled: false,
|
|
201
|
+
autoHide: true,
|
|
202
|
+
autoHideDelay: 5000,
|
|
203
|
+
showChapterMarkers: true,
|
|
204
|
+
skipButtonPosition: 'bottom-right',
|
|
205
|
+
customStyles: {},
|
|
206
|
+
userPreferences: {
|
|
207
|
+
autoSkipIntro: false,
|
|
208
|
+
autoSkipRecap: false,
|
|
209
|
+
autoSkipCredits: false,
|
|
210
|
+
showSkipButtons: true,
|
|
211
|
+
skipButtonTimeout: 5000,
|
|
212
|
+
rememberChoices: true
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Default skip button labels for different segment types
|
|
218
|
+
*/
|
|
219
|
+
export const DEFAULT_SKIP_LABELS: Record<SegmentType, string> = {
|
|
220
|
+
intro: 'Skip Intro',
|
|
221
|
+
recap: 'Skip Recap',
|
|
222
|
+
content: 'Skip',
|
|
223
|
+
credits: 'Skip Credits',
|
|
224
|
+
ad: 'Skip Ad'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Colors for different segment types in chapter markers
|
|
229
|
+
*/
|
|
230
|
+
export const SEGMENT_COLORS: Record<SegmentType, string> = {
|
|
231
|
+
intro: '#ff5722', // Orange-red
|
|
232
|
+
recap: '#ffc107', // Amber
|
|
233
|
+
content: '#4caf50', // Green
|
|
234
|
+
credits: '#9c27b0', // Purple
|
|
235
|
+
ad: '#f44336' // Red
|
|
236
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
|
-
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '
|
|
4
|
+
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from 'unified-video-core';
|
|
5
5
|
import { WebPlayer } from '../WebPlayer';
|
|
6
6
|
// EPG imports - conditionally loaded
|
|
7
7
|
import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow } from './types/EPGTypes';
|
|
@@ -67,7 +67,7 @@ export type WebPlayerViewProps = {
|
|
|
67
67
|
};
|
|
68
68
|
};
|
|
69
69
|
// Paywall with Email Authentication
|
|
70
|
-
paywall?: import('
|
|
70
|
+
paywall?: import('unified-video-core').PaywallConfig;
|
|
71
71
|
paywallConfigUrl?: string; // optional endpoint returning PaywallConfig JSON
|
|
72
72
|
|
|
73
73
|
// Email Authentication (will be merged into paywall config)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
|
-
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '
|
|
4
|
+
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from 'unified-video-core';
|
|
5
5
|
import { WebPlayer } from '../WebPlayer';
|
|
6
6
|
import EPGOverlay from './components/EPGOverlay';
|
|
7
7
|
import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow, EPGAction } from './types/EPGTypes';
|
|
@@ -13,7 +13,7 @@ export interface WebPlayerViewWithEPGProps {
|
|
|
13
13
|
enableAdaptiveBitrate?: boolean;
|
|
14
14
|
debug?: boolean;
|
|
15
15
|
freeDuration?: number;
|
|
16
|
-
paywall?: import('
|
|
16
|
+
paywall?: import('unified-video-core').PaywallConfig;
|
|
17
17
|
paywallConfigUrl?: string;
|
|
18
18
|
|
|
19
19
|
emailAuth?: {
|
|
@@ -473,4 +473,4 @@ export const WebPlayerViewWithEPG: React.FC<WebPlayerViewWithEPGProps> = (props)
|
|
|
473
473
|
);
|
|
474
474
|
};
|
|
475
475
|
|
|
476
|
-
export default WebPlayerViewWithEPG;
|
|
476
|
+
export default WebPlayerViewWithEPG;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React component for progress bar with chapter markers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
VideoSegment,
|
|
8
|
+
VideoChapters,
|
|
9
|
+
SEGMENT_COLORS
|
|
10
|
+
} from '../../chapters/types/ChapterTypes';
|
|
11
|
+
|
|
12
|
+
export interface ChapterMarker {
|
|
13
|
+
segment: VideoSegment;
|
|
14
|
+
position: number; // Percentage position (0-100)
|
|
15
|
+
color: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChapterProgressProps {
|
|
20
|
+
/** Chapter data */
|
|
21
|
+
chapters: VideoChapters | null;
|
|
22
|
+
|
|
23
|
+
/** Current playback progress (0-100) */
|
|
24
|
+
progress?: number;
|
|
25
|
+
|
|
26
|
+
/** Buffered progress (0-100) */
|
|
27
|
+
buffered?: number;
|
|
28
|
+
|
|
29
|
+
/** Whether to show chapter markers */
|
|
30
|
+
showMarkers?: boolean;
|
|
31
|
+
|
|
32
|
+
/** Custom marker colors */
|
|
33
|
+
markerColors?: Partial<typeof SEGMENT_COLORS>;
|
|
34
|
+
|
|
35
|
+
/** Callback when marker is clicked */
|
|
36
|
+
onMarkerClick?: (segment: VideoSegment) => void;
|
|
37
|
+
|
|
38
|
+
/** Callback when progress bar is clicked */
|
|
39
|
+
onProgressClick?: (percentage: number) => void;
|
|
40
|
+
|
|
41
|
+
/** Custom CSS class name */
|
|
42
|
+
className?: string;
|
|
43
|
+
|
|
44
|
+
/** Custom styles */
|
|
45
|
+
style?: React.CSSProperties;
|
|
46
|
+
|
|
47
|
+
/** Whether progress bar is interactive */
|
|
48
|
+
interactive?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ChapterProgress: React.FC<ChapterProgressProps> = ({
|
|
52
|
+
chapters,
|
|
53
|
+
progress = 0,
|
|
54
|
+
buffered = 0,
|
|
55
|
+
showMarkers = true,
|
|
56
|
+
markerColors = {},
|
|
57
|
+
onMarkerClick,
|
|
58
|
+
onProgressClick,
|
|
59
|
+
className = '',
|
|
60
|
+
style = {},
|
|
61
|
+
interactive = true
|
|
62
|
+
}) => {
|
|
63
|
+
/**
|
|
64
|
+
* Generate chapter markers
|
|
65
|
+
*/
|
|
66
|
+
const markers = useMemo((): ChapterMarker[] => {
|
|
67
|
+
if (!chapters || !showMarkers) return [];
|
|
68
|
+
|
|
69
|
+
const colors = { ...SEGMENT_COLORS, ...markerColors };
|
|
70
|
+
|
|
71
|
+
return chapters.segments
|
|
72
|
+
.filter(segment => segment.type !== 'content') // Don't show markers for content
|
|
73
|
+
.map(segment => ({
|
|
74
|
+
segment,
|
|
75
|
+
position: (segment.startTime / chapters.duration) * 100,
|
|
76
|
+
color: colors[segment.type] || '#ffffff',
|
|
77
|
+
label: segment.title || segment.type
|
|
78
|
+
}))
|
|
79
|
+
.sort((a, b) => a.position - b.position);
|
|
80
|
+
}, [chapters, showMarkers, markerColors]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle progress bar click
|
|
84
|
+
*/
|
|
85
|
+
const handleProgressClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
86
|
+
if (!interactive || !onProgressClick) return;
|
|
87
|
+
|
|
88
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
89
|
+
const x = event.clientX - rect.left;
|
|
90
|
+
const percentage = (x / rect.width) * 100;
|
|
91
|
+
|
|
92
|
+
onProgressClick(Math.max(0, Math.min(100, percentage)));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle marker click
|
|
97
|
+
*/
|
|
98
|
+
const handleMarkerClick = (event: React.MouseEvent, segment: VideoSegment) => {
|
|
99
|
+
event.stopPropagation(); // Prevent progress bar click
|
|
100
|
+
onMarkerClick?.(segment);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Combine classes
|
|
104
|
+
const progressClasses = [
|
|
105
|
+
'uvf-chapter-progress',
|
|
106
|
+
interactive ? 'interactive' : '',
|
|
107
|
+
className
|
|
108
|
+
].filter(Boolean).join(' ');
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
className={progressClasses}
|
|
113
|
+
style={{
|
|
114
|
+
position: 'relative',
|
|
115
|
+
width: '100%',
|
|
116
|
+
height: '4px',
|
|
117
|
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
118
|
+
borderRadius: '2px',
|
|
119
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
120
|
+
...style
|
|
121
|
+
}}
|
|
122
|
+
onClick={handleProgressClick}
|
|
123
|
+
>
|
|
124
|
+
{/* Buffered progress */}
|
|
125
|
+
<div
|
|
126
|
+
className="uvf-chapter-progress-buffered"
|
|
127
|
+
style={{
|
|
128
|
+
position: 'absolute',
|
|
129
|
+
top: 0,
|
|
130
|
+
left: 0,
|
|
131
|
+
height: '100%',
|
|
132
|
+
width: `${Math.max(0, Math.min(100, buffered))}%`,
|
|
133
|
+
backgroundColor: 'rgba(255, 255, 255, 0.4)',
|
|
134
|
+
borderRadius: 'inherit',
|
|
135
|
+
transition: 'width 0.3s ease'
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
{/* Current progress */}
|
|
140
|
+
<div
|
|
141
|
+
className="uvf-chapter-progress-current"
|
|
142
|
+
style={{
|
|
143
|
+
position: 'absolute',
|
|
144
|
+
top: 0,
|
|
145
|
+
left: 0,
|
|
146
|
+
height: '100%',
|
|
147
|
+
width: `${Math.max(0, Math.min(100, progress))}%`,
|
|
148
|
+
background: 'linear-gradient(90deg, var(--uvf-accent-1, #ff5722) 0%, var(--uvf-accent-2, #ff8a50) 100%)',
|
|
149
|
+
borderRadius: 'inherit',
|
|
150
|
+
transition: 'width 0.1s ease'
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{/* Chapter markers */}
|
|
155
|
+
{markers.map((marker, index) => (
|
|
156
|
+
<div
|
|
157
|
+
key={`${marker.segment.id}-${index}`}
|
|
158
|
+
className={`uvf-chapter-progress-marker uvf-marker-${marker.segment.type}`}
|
|
159
|
+
style={{
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
top: '50%',
|
|
162
|
+
left: `${marker.position}%`,
|
|
163
|
+
width: '3px',
|
|
164
|
+
height: '150%',
|
|
165
|
+
backgroundColor: marker.color,
|
|
166
|
+
transform: 'translate(-50%, -50%)',
|
|
167
|
+
cursor: 'pointer',
|
|
168
|
+
borderRadius: '1px',
|
|
169
|
+
zIndex: 10,
|
|
170
|
+
transition: 'all 0.2s ease'
|
|
171
|
+
}}
|
|
172
|
+
title={`${marker.label} (${formatTime(marker.segment.startTime)})`}
|
|
173
|
+
onClick={(e) => handleMarkerClick(e, marker.segment)}
|
|
174
|
+
onMouseEnter={(e) => {
|
|
175
|
+
const element = e.currentTarget as HTMLElement;
|
|
176
|
+
element.style.width = '4px';
|
|
177
|
+
element.style.height = '200%';
|
|
178
|
+
element.style.boxShadow = `0 0 8px ${marker.color}`;
|
|
179
|
+
}}
|
|
180
|
+
onMouseLeave={(e) => {
|
|
181
|
+
const element = e.currentTarget as HTMLElement;
|
|
182
|
+
element.style.width = '3px';
|
|
183
|
+
element.style.height = '150%';
|
|
184
|
+
element.style.boxShadow = 'none';
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Format time in MM:SS or HH:MM:SS format
|
|
194
|
+
*/
|
|
195
|
+
function formatTime(seconds: number): string {
|
|
196
|
+
if (!seconds || isNaN(seconds)) return '0:00';
|
|
197
|
+
|
|
198
|
+
const hours = Math.floor(seconds / 3600);
|
|
199
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
200
|
+
const secs = Math.floor(seconds % 60);
|
|
201
|
+
|
|
202
|
+
if (hours > 0) {
|
|
203
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
204
|
+
} else {
|
|
205
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
206
|
+
}
|
|
207
|
+
}
|