@therealtinhtute/baroiplayer 1.0.0
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/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/context/media.js +2 -0
- package/dist/context/media.js.map +1 -0
- package/dist/context/media.mjs +15 -0
- package/dist/context/media.mjs.map +1 -0
- package/dist/context/store.js +2 -0
- package/dist/context/store.js.map +1 -0
- package/dist/context/store.mjs +5 -0
- package/dist/context/store.mjs.map +1 -0
- package/dist/context-BMteAJDN.js +1140 -0
- package/dist/context-BMteAJDN.js.map +1 -0
- package/dist/context-DCJ_scmO.cjs +23 -0
- package/dist/context-DCJ_scmO.cjs.map +1 -0
- package/dist/core-events-B5W_j8yU.js +480 -0
- package/dist/core-events-B5W_j8yU.js.map +1 -0
- package/dist/core-events-CN0-Vhhq.cjs +2 -0
- package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
- package/dist/core-plugins-Bq9KLany.js +696 -0
- package/dist/core-plugins-Bq9KLany.js.map +1 -0
- package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
- package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
- package/dist/core.js +37 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +1217 -0
- package/dist/core.mjs.map +1 -0
- package/dist/enhanced-components-DHv0MFvG.js +1073 -0
- package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
- package/dist/enhanced-components-DTIObgB7.cjs +2 -0
- package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
- package/dist/hooks-C_r8wexn.cjs +4 -0
- package/dist/hooks-C_r8wexn.cjs.map +1 -0
- package/dist/hooks-Da2wVXlF.js +1630 -0
- package/dist/hooks-Da2wVXlF.js.map +1 -0
- package/dist/index.css +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6126 -0
- package/dist/index.mjs.map +1 -0
- package/dist/minimal-components-DSq5sQpy.js +2002 -0
- package/dist/minimal-components-DSq5sQpy.js.map +1 -0
- package/dist/minimal-components-DflWiihN.cjs +2 -0
- package/dist/minimal-components-DflWiihN.cjs.map +1 -0
- package/dist/minimal-components.css +1 -0
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
- package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
- package/dist/performance-monitor-rMW1RgV3.js +302 -0
- package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
- package/dist/utils-BnscpYYd.js +2798 -0
- package/dist/utils-BnscpYYd.js.map +1 -0
- package/dist/utils-D_czS_0K.cjs +9 -0
- package/dist/utils-D_czS_0K.cjs.map +1 -0
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +114 -0
- package/src/adapters/videojs-adapter.tsx +470 -0
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +115 -0
- package/src/components/LoadingState.tsx +248 -0
- package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
- package/src/components/audio-player.tsx +85 -0
- package/src/components/basic/index.ts +37 -0
- package/src/components/enhanced/advanced-features.tsx +316 -0
- package/src/components/enhanced/debug-panel.tsx +422 -0
- package/src/components/enhanced/enhanced-controls.tsx +600 -0
- package/src/components/enhanced/enhanced-media-player.tsx +509 -0
- package/src/components/enhanced/error-handling.tsx +345 -0
- package/src/components/enhanced/index.ts +39 -0
- package/src/components/enhanced/media-player-enhanced.tsx +349 -0
- package/src/components/examples/basic-video-player.tsx +55 -0
- package/src/components/examples/custom-controls-example.tsx +63 -0
- package/src/components/examples/enhanced-controls-example.tsx +114 -0
- package/src/components/examples/enhanced-media-player-example.tsx +293 -0
- package/src/components/examples/loading-states-demo.tsx +142 -0
- package/src/components/examples/react-19-demo.tsx +381 -0
- package/src/components/examples/volume-orientation-demo.tsx +114 -0
- package/src/components/icon-demo.tsx +209 -0
- package/src/components/icons/index.tsx +166 -0
- package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
- package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
- package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
- package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
- package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
- package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
- package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
- package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
- package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
- package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
- package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
- package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
- package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
- package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
- package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
- package/src/components/legacy.tsx +93 -0
- package/src/components/media-metadata-display.tsx +100 -0
- package/src/components/media-player-audio.tsx +39 -0
- package/src/components/media-player-controls.tsx +226 -0
- package/src/components/media-player-error.tsx +163 -0
- package/src/components/media-player-fullscreen.tsx +109 -0
- package/src/components/media-player-hls.tsx +111 -0
- package/src/components/media-player-loading.tsx +76 -0
- package/src/components/media-player-play.tsx +75 -0
- package/src/components/media-player-root.tsx +340 -0
- package/src/components/media-player-seek.tsx +168 -0
- package/src/components/media-player-time.tsx +99 -0
- package/src/components/media-player-tooltip.tsx +58 -0
- package/src/components/media-player-video.tsx +39 -0
- package/src/components/media-player-volume.tsx +155 -0
- package/src/components/media-player.tsx +257 -0
- package/src/components/media-suspense.tsx +362 -0
- package/src/components/player-settings-form.tsx +163 -0
- package/src/components/react-player-wrapper.tsx +651 -0
- package/src/components/simple-video-player.tsx +60 -0
- package/src/components/ui/enhanced-seek-bar.tsx +235 -0
- package/src/components/ui/fullscreen-button.tsx +60 -0
- package/src/components/ui/media-resource-preloader.tsx +395 -0
- package/src/components/ui/play-button.tsx +107 -0
- package/src/components/ui/seek-bar.tsx +161 -0
- package/src/components/ui/time-display.tsx +40 -0
- package/src/components/ui/volume-control.tsx +143 -0
- package/src/components/unified/base-media-player.tsx +246 -0
- package/src/components/unified/media-player.tsx +75 -0
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
- package/src/components/variants/minimal/MinimalControls.tsx +142 -0
- package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
- package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
- package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
- package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
- package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
- package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
- package/src/components/variants/minimal/components/RightControls.tsx +173 -0
- package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
- package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
- package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
- package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
- package/src/components/variants/minimal/index.ts +23 -0
- package/src/components/video-player.tsx +100 -0
- package/src/context/error-handling.ts +394 -0
- package/src/context/media-context.tsx +208 -0
- package/src/context/media-provider.tsx +265 -0
- package/src/context/media-store.ts +379 -0
- package/src/context/persistence.ts +453 -0
- package/src/core/events/event-system.ts +442 -0
- package/src/core/events/hooks.ts +302 -0
- package/src/core/events/index.ts +22 -0
- package/src/core/events/middleware.ts +269 -0
- package/src/core/index.ts +22 -0
- package/src/core/optimization/bundle-optimizer.tsx +308 -0
- package/src/core/performance/cicd.ts +96 -0
- package/src/core/performance/index.ts +38 -0
- package/src/core/performance/media-loading-optimizer.ts +538 -0
- package/src/core/performance/performance-budgets.ts +432 -0
- package/src/core/performance/performance-monitor.tsx +476 -0
- package/src/core/performance/performance-testing.ts +459 -0
- package/src/core/plugins/base-plugin.ts +224 -0
- package/src/core/plugins/enhanced-plugin-system.ts +411 -0
- package/src/core/plugins/examples/analytics-plugin.ts +242 -0
- package/src/core/plugins/index.ts +39 -0
- package/src/core/plugins/registry.ts +327 -0
- package/src/core/plugins/types.ts +159 -0
- package/src/core/plugins/utils.ts +316 -0
- package/src/core/progressive-loading.tsx +322 -0
- package/src/core/providers/enhanced-hls-provider.ts +339 -0
- package/src/core/providers/service-provider.tsx +168 -0
- package/src/core/utils.tsx +38 -0
- package/src/features/media-session/media-session-manager.ts +429 -0
- package/src/features/picture-in-picture/pip-manager.ts +429 -0
- package/src/features/playlist/playlist-manager.ts +606 -0
- package/src/features/subtitles/subtitle-manager.ts +441 -0
- package/src/features/subtitles/subtitle-parser.ts +280 -0
- package/src/features/thumbnails/thumbnail-generator.ts +519 -0
- package/src/hooks/accessibility/index.ts +27 -0
- package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
- package/src/hooks/accessibility/use-focus-management.ts +176 -0
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
- package/src/hooks/accessibility/use-screen-reader.ts +182 -0
- package/src/hooks/core/index.ts +47 -0
- package/src/hooks/legacy.ts +27 -0
- package/src/hooks/use-enhanced-media-player.ts +468 -0
- package/src/hooks/use-intelligent-preloading.ts +358 -0
- package/src/hooks/use-keyboard-controls.ts +115 -0
- package/src/hooks/use-media-actions.ts +175 -0
- package/src/hooks/use-media-metadata.ts +89 -0
- package/src/hooks/use-media-optimistic.ts +270 -0
- package/src/hooks/use-media-player.ts +312 -0
- package/src/hooks/use-media-state.ts +190 -0
- package/src/hooks/use-memory-optimization.ts +358 -0
- package/src/hooks/use-optimistic-controls.ts +92 -0
- package/src/hooks/use-optimized-media.ts +263 -0
- package/src/hooks/use-touch-gestures.ts +417 -0
- package/src/hooks/useEnhancedSubtitles.ts +258 -0
- package/src/hooks/useKeyboardShortcuts.ts +74 -0
- package/src/hooks/useMediaControls.ts +52 -0
- package/src/hooks/useScreenshot.ts +57 -0
- package/src/hooks/useSubtitles.ts +91 -0
- package/src/hooks/useVideoPlayer.ts +232 -0
- package/src/icons/components/FullscreenIcon.tsx +37 -0
- package/src/icons/components/SettingsIcon.tsx +38 -0
- package/src/icons/components/animated/LoadingIcon.tsx +43 -0
- package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
- package/src/icons/components/media/PauseIcon.tsx +39 -0
- package/src/icons/components/media/PlayIcon.tsx +38 -0
- package/src/icons/components/volume/VolumeIcon.tsx +39 -0
- package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
- package/src/icons/index.ts +56 -0
- package/src/icons/provider.tsx +64 -0
- package/src/icons/types.ts +40 -0
- package/src/index-enhanced.ts +35 -0
- package/src/index-legacy.ts +326 -0
- package/src/index.css +102 -0
- package/src/index.ts +216 -0
- package/src/lib/deprecation.ts +51 -0
- package/src/lib/format-time.ts +37 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/globals.css +462 -0
- package/src/styles/minimal-player.css +589 -0
- package/src/styles/pixel-art.css +46 -0
- package/src/styles/videojs.css +100 -0
- package/src/test/setup.ts +79 -0
- package/src/types/events.ts +47 -0
- package/src/types/formats.ts +95 -0
- package/src/types/index.ts +190 -0
- package/src/types/videojs-plugins.d.ts +14 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { SubtitleParser, type SubtitleCue, type ParsedSubtitles } from './subtitle-parser'
|
|
2
|
+
|
|
3
|
+
export interface SubtitleTrack {
|
|
4
|
+
id: string
|
|
5
|
+
url: string
|
|
6
|
+
label: string
|
|
7
|
+
language: string
|
|
8
|
+
isDefault?: boolean
|
|
9
|
+
format?: 'vtt' | 'srt'
|
|
10
|
+
parsedSubtitles?: ParsedSubtitles
|
|
11
|
+
loadingState?: 'idle' | 'loading' | 'loaded' | 'error'
|
|
12
|
+
error?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubtitleStyle {
|
|
16
|
+
fontSize?: number
|
|
17
|
+
fontFamily?: string
|
|
18
|
+
color?: string
|
|
19
|
+
backgroundColor?: string
|
|
20
|
+
borderColor?: string
|
|
21
|
+
borderWidth?: number
|
|
22
|
+
opacity?: number
|
|
23
|
+
position?: 'top' | 'center' | 'bottom'
|
|
24
|
+
alignment?: 'left' | 'center' | 'right'
|
|
25
|
+
shadow?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SubtitleManagerOptions {
|
|
29
|
+
tracks?: SubtitleTrack[]
|
|
30
|
+
defaultStyle?: SubtitleStyle
|
|
31
|
+
enabledByDefault?: boolean
|
|
32
|
+
preloadTracks?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SubtitleManager {
|
|
36
|
+
private tracks: Map<string, SubtitleTrack> = new Map()
|
|
37
|
+
private activeTrackId: string | null = null
|
|
38
|
+
private currentCues: SubtitleCue[] = []
|
|
39
|
+
private isEnabled: boolean = false
|
|
40
|
+
private style: SubtitleStyle
|
|
41
|
+
private eventListeners: Map<string, Set<Function>> = new Map()
|
|
42
|
+
|
|
43
|
+
constructor(options: SubtitleManagerOptions = {}) {
|
|
44
|
+
this.style = {
|
|
45
|
+
fontSize: 16,
|
|
46
|
+
fontFamily: 'Arial, sans-serif',
|
|
47
|
+
color: '#ffffff',
|
|
48
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
49
|
+
borderColor: '#000000',
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
opacity: 1,
|
|
52
|
+
position: 'bottom',
|
|
53
|
+
alignment: 'center',
|
|
54
|
+
shadow: true,
|
|
55
|
+
...options.defaultStyle
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.isEnabled = options.enabledByDefault ?? false
|
|
59
|
+
|
|
60
|
+
if (options.tracks) {
|
|
61
|
+
this.addTracks(options.tracks)
|
|
62
|
+
|
|
63
|
+
if (options.preloadTracks) {
|
|
64
|
+
this.preloadAllTracks()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add multiple subtitle tracks
|
|
71
|
+
*/
|
|
72
|
+
addTracks(tracks: SubtitleTrack[]): void {
|
|
73
|
+
tracks.forEach(track => this.addTrack(track))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add a single subtitle track
|
|
78
|
+
*/
|
|
79
|
+
addTrack(track: SubtitleTrack): void {
|
|
80
|
+
const trackWithDefaults: SubtitleTrack = {
|
|
81
|
+
...track,
|
|
82
|
+
loadingState: 'idle',
|
|
83
|
+
format: track.format || this.detectFormat(track.url)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.tracks.set(track.id, trackWithDefaults)
|
|
87
|
+
this.emit('trackAdded', trackWithDefaults)
|
|
88
|
+
|
|
89
|
+
// Set as active if it's the default track and no track is currently active
|
|
90
|
+
if (track.isDefault && !this.activeTrackId) {
|
|
91
|
+
this.setActiveTrack(track.id)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove a subtitle track
|
|
97
|
+
*/
|
|
98
|
+
removeTrack(trackId: string): void {
|
|
99
|
+
const track = this.tracks.get(trackId)
|
|
100
|
+
if (track) {
|
|
101
|
+
this.tracks.delete(trackId)
|
|
102
|
+
|
|
103
|
+
if (this.activeTrackId === trackId) {
|
|
104
|
+
this.activeTrackId = null
|
|
105
|
+
this.currentCues = []
|
|
106
|
+
this.emit('cuesChanged', [])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.emit('trackRemoved', track)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all available tracks
|
|
115
|
+
*/
|
|
116
|
+
getTracks(): SubtitleTrack[] {
|
|
117
|
+
return Array.from(this.tracks.values())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a specific track by ID
|
|
122
|
+
*/
|
|
123
|
+
getTrack(trackId: string): SubtitleTrack | undefined {
|
|
124
|
+
return this.tracks.get(trackId)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Set the active subtitle track
|
|
129
|
+
*/
|
|
130
|
+
async setActiveTrack(trackId: string | null): Promise<void> {
|
|
131
|
+
this.activeTrackId = trackId
|
|
132
|
+
this.currentCues = []
|
|
133
|
+
|
|
134
|
+
if (trackId) {
|
|
135
|
+
const track = this.tracks.get(trackId)
|
|
136
|
+
if (track && !track.parsedSubtitles && track.loadingState === 'idle') {
|
|
137
|
+
await this.loadTrack(trackId)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.emit('activeTrackChanged', trackId)
|
|
142
|
+
this.emit('cuesChanged', [])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the currently active track
|
|
147
|
+
*/
|
|
148
|
+
getActiveTrack(): SubtitleTrack | null {
|
|
149
|
+
return this.activeTrackId ? this.tracks.get(this.activeTrackId) || null : null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Enable or disable subtitles
|
|
154
|
+
*/
|
|
155
|
+
setEnabled(enabled: boolean): void {
|
|
156
|
+
this.isEnabled = enabled
|
|
157
|
+
|
|
158
|
+
if (!enabled) {
|
|
159
|
+
this.currentCues = []
|
|
160
|
+
this.emit('cuesChanged', [])
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.emit('enabledChanged', enabled)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if subtitles are enabled
|
|
168
|
+
*/
|
|
169
|
+
isSubtitlesEnabled(): boolean {
|
|
170
|
+
return this.isEnabled
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update subtitle styling
|
|
175
|
+
*/
|
|
176
|
+
updateStyle(newStyle: Partial<SubtitleStyle>): void {
|
|
177
|
+
this.style = { ...this.style, ...newStyle }
|
|
178
|
+
this.emit('styleChanged', this.style)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get current subtitle style
|
|
183
|
+
*/
|
|
184
|
+
getStyle(): SubtitleStyle {
|
|
185
|
+
return { ...this.style }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update current time and get active cues
|
|
190
|
+
*/
|
|
191
|
+
updateTime(currentTime: number): SubtitleCue[] {
|
|
192
|
+
if (!this.isEnabled || !this.activeTrackId) {
|
|
193
|
+
if (this.currentCues.length > 0) {
|
|
194
|
+
this.currentCues = []
|
|
195
|
+
this.emit('cuesChanged', [])
|
|
196
|
+
}
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const activeTrack = this.tracks.get(this.activeTrackId)
|
|
201
|
+
if (!activeTrack?.parsedSubtitles) {
|
|
202
|
+
if (this.currentCues.length > 0) {
|
|
203
|
+
this.currentCues = []
|
|
204
|
+
this.emit('cuesChanged', [])
|
|
205
|
+
}
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const activeCues = SubtitleParser.getActiveCues(
|
|
210
|
+
activeTrack.parsedSubtitles.cues,
|
|
211
|
+
currentTime
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// Check if cues have changed
|
|
215
|
+
const cuesChanged =
|
|
216
|
+
activeCues.length !== this.currentCues.length ||
|
|
217
|
+
activeCues.some((cue, index) =>
|
|
218
|
+
!this.currentCues[index] || cue.id !== this.currentCues[index].id
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (cuesChanged) {
|
|
222
|
+
this.currentCues = activeCues
|
|
223
|
+
this.emit('cuesChanged', activeCues)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return activeCues
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get current active cues
|
|
231
|
+
*/
|
|
232
|
+
getCurrentCues(): SubtitleCue[] {
|
|
233
|
+
return [...this.currentCues]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Load a subtitle track
|
|
238
|
+
*/
|
|
239
|
+
async loadTrack(trackId: string): Promise<void> {
|
|
240
|
+
const track = this.tracks.get(trackId)
|
|
241
|
+
if (!track || track.loadingState === 'loading' || track.loadingState === 'loaded') {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update loading state
|
|
246
|
+
track.loadingState = 'loading'
|
|
247
|
+
this.emit('trackLoadingStateChanged', { trackId, state: 'loading' })
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await fetch(track.url)
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
throw new Error(`Failed to fetch subtitles: ${response.status} ${response.statusText}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const content = await response.text()
|
|
256
|
+
const format = track.format || SubtitleParser.detectFormat(content)
|
|
257
|
+
|
|
258
|
+
if (format === 'unknown') {
|
|
259
|
+
throw new Error('Unsupported subtitle format')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const parsedSubtitles = SubtitleParser.parse(content, format)
|
|
263
|
+
track.parsedSubtitles = parsedSubtitles
|
|
264
|
+
track.loadingState = 'loaded'
|
|
265
|
+
track.format = format
|
|
266
|
+
|
|
267
|
+
this.emit('trackLoadingStateChanged', { trackId, state: 'loaded' })
|
|
268
|
+
this.emit('trackLoaded', track)
|
|
269
|
+
|
|
270
|
+
} catch (error) {
|
|
271
|
+
track.loadingState = 'error'
|
|
272
|
+
track.error = error instanceof Error ? error.message : 'Unknown error'
|
|
273
|
+
|
|
274
|
+
this.emit('trackLoadingStateChanged', { trackId, state: 'error', error: track.error })
|
|
275
|
+
this.emit('trackError', { trackId, error: track.error })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Preload all available tracks
|
|
281
|
+
*/
|
|
282
|
+
async preloadAllTracks(): Promise<void> {
|
|
283
|
+
const loadPromises = Array.from(this.tracks.keys()).map(trackId =>
|
|
284
|
+
this.loadTrack(trackId)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
await Promise.allSettled(loadPromises)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Search for cues containing text
|
|
292
|
+
*/
|
|
293
|
+
searchCues(query: string, trackId?: string): Array<{ trackId: string, cue: SubtitleCue }> {
|
|
294
|
+
const results: Array<{ trackId: string, cue: SubtitleCue }> = []
|
|
295
|
+
const searchTracks = trackId ? [trackId] : Array.from(this.tracks.keys())
|
|
296
|
+
|
|
297
|
+
for (const id of searchTracks) {
|
|
298
|
+
const track = this.tracks.get(id)
|
|
299
|
+
if (!track?.parsedSubtitles) continue
|
|
300
|
+
|
|
301
|
+
for (const cue of track.parsedSubtitles.cues) {
|
|
302
|
+
const cleanText = SubtitleParser.cleanText(cue.text).toLowerCase()
|
|
303
|
+
if (cleanText.includes(query.toLowerCase())) {
|
|
304
|
+
results.push({ trackId: id, cue })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return results
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Export subtitles in a specific format
|
|
314
|
+
*/
|
|
315
|
+
exportSubtitles(trackId: string, format: 'vtt' | 'srt'): string | null {
|
|
316
|
+
const track = this.tracks.get(trackId)
|
|
317
|
+
if (!track?.parsedSubtitles) return null
|
|
318
|
+
|
|
319
|
+
const { cues, metadata } = track.parsedSubtitles
|
|
320
|
+
|
|
321
|
+
if (format === 'vtt') {
|
|
322
|
+
return this.exportAsVTT(cues, metadata)
|
|
323
|
+
} else {
|
|
324
|
+
return this.exportAsSRT(cues)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Event system methods
|
|
330
|
+
*/
|
|
331
|
+
on(event: string, callback: Function): () => void {
|
|
332
|
+
if (!this.eventListeners.has(event)) {
|
|
333
|
+
this.eventListeners.set(event, new Set())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.eventListeners.get(event)!.add(callback)
|
|
337
|
+
|
|
338
|
+
return () => {
|
|
339
|
+
const listeners = this.eventListeners.get(event)
|
|
340
|
+
if (listeners) {
|
|
341
|
+
listeners.delete(callback)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private emit(event: string, data?: any): void {
|
|
347
|
+
const listeners = this.eventListeners.get(event)
|
|
348
|
+
if (listeners) {
|
|
349
|
+
listeners.forEach(callback => callback(data))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Detect subtitle format from URL extension
|
|
355
|
+
*/
|
|
356
|
+
private detectFormat(url: string): 'vtt' | 'srt' {
|
|
357
|
+
const extension = url.split('.').pop()?.toLowerCase()
|
|
358
|
+
return extension === 'srt' ? 'srt' : 'vtt'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Export cues as WebVTT format
|
|
363
|
+
*/
|
|
364
|
+
private exportAsVTT(cues: SubtitleCue[], metadata?: any): string {
|
|
365
|
+
let output = 'WEBVTT\n\n'
|
|
366
|
+
|
|
367
|
+
if (metadata) {
|
|
368
|
+
Object.entries(metadata).forEach(([key, value]) => {
|
|
369
|
+
output += `${key}: ${value}\n`
|
|
370
|
+
})
|
|
371
|
+
output += '\n'
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
cues.forEach((cue, index) => {
|
|
375
|
+
if (cue.id) {
|
|
376
|
+
output += `${cue.id}\n`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const startTime = this.formatTime(cue.startTime, 'vtt')
|
|
380
|
+
const endTime = this.formatTime(cue.endTime, 'vtt')
|
|
381
|
+
|
|
382
|
+
output += `${startTime} --> ${endTime}`
|
|
383
|
+
|
|
384
|
+
if (cue.settings) {
|
|
385
|
+
const settings = []
|
|
386
|
+
Object.entries(cue.settings).forEach(([key, value]) => {
|
|
387
|
+
settings.push(`${key}:${value}`)
|
|
388
|
+
})
|
|
389
|
+
if (settings.length > 0) {
|
|
390
|
+
output += ` ${settings.join(' ')}`
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
output += `\n${cue.text}\n\n`
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
return output
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Export cues as SRT format
|
|
402
|
+
*/
|
|
403
|
+
private exportAsSRT(cues: SubtitleCue[]): string {
|
|
404
|
+
let output = ''
|
|
405
|
+
|
|
406
|
+
cues.forEach((cue, index) => {
|
|
407
|
+
const startTime = this.formatTime(cue.startTime, 'srt')
|
|
408
|
+
const endTime = this.formatTime(cue.endTime, 'srt')
|
|
409
|
+
|
|
410
|
+
output += `${index + 1}\n`
|
|
411
|
+
output += `${startTime} --> ${endTime}\n`
|
|
412
|
+
output += `${cue.text}\n\n`
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
return output
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Format time for different subtitle formats
|
|
420
|
+
*/
|
|
421
|
+
private formatTime(seconds: number, format: 'vtt' | 'srt'): string {
|
|
422
|
+
const hours = Math.floor(seconds / 3600)
|
|
423
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
424
|
+
const secs = Math.floor(seconds % 60)
|
|
425
|
+
const milliseconds = Math.floor((seconds % 1) * 1000)
|
|
426
|
+
|
|
427
|
+
const separator = format === 'srt' ? ',' : '.'
|
|
428
|
+
|
|
429
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}${separator}${milliseconds.toString().padStart(3, '0')}`
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Cleanup resources
|
|
434
|
+
*/
|
|
435
|
+
destroy(): void {
|
|
436
|
+
this.tracks.clear()
|
|
437
|
+
this.eventListeners.clear()
|
|
438
|
+
this.activeTrackId = null
|
|
439
|
+
this.currentCues = []
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
export interface SubtitleCue {
|
|
2
|
+
id?: string
|
|
3
|
+
startTime: number
|
|
4
|
+
endTime: number
|
|
5
|
+
text: string
|
|
6
|
+
settings?: {
|
|
7
|
+
line?: number | 'auto'
|
|
8
|
+
position?: number
|
|
9
|
+
size?: number
|
|
10
|
+
align?: 'start' | 'center' | 'end'
|
|
11
|
+
vertical?: 'rl' | 'lr'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ParsedSubtitles {
|
|
16
|
+
cues: SubtitleCue[]
|
|
17
|
+
metadata?: {
|
|
18
|
+
title?: string
|
|
19
|
+
language?: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SubtitleParser {
|
|
25
|
+
/**
|
|
26
|
+
* Parse subtitle content based on format
|
|
27
|
+
*/
|
|
28
|
+
static parse(content: string, format: 'vtt' | 'srt'): ParsedSubtitles {
|
|
29
|
+
switch (format) {
|
|
30
|
+
case 'vtt':
|
|
31
|
+
return this.parseVTT(content)
|
|
32
|
+
case 'srt':
|
|
33
|
+
return this.parseSRT(content)
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`Unsupported subtitle format: ${format}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Auto-detect subtitle format from content
|
|
41
|
+
*/
|
|
42
|
+
static detectFormat(content: string): 'vtt' | 'srt' | 'unknown' {
|
|
43
|
+
const trimmedContent = content.trim()
|
|
44
|
+
|
|
45
|
+
if (trimmedContent.startsWith('WEBVTT')) {
|
|
46
|
+
return 'vtt'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for SRT pattern (number, timestamp, text)
|
|
50
|
+
const srtPattern = /^\d+\s*\n\d{2}:\d{2}:\d{2},\d{3}\s*-->\s*\d{2}:\d{2}:\d{2},\d{3}/m
|
|
51
|
+
if (srtPattern.test(trimmedContent)) {
|
|
52
|
+
return 'srt'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'unknown'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse WebVTT format
|
|
60
|
+
*/
|
|
61
|
+
private static parseVTT(content: string): ParsedSubtitles {
|
|
62
|
+
const lines = content.split('\n')
|
|
63
|
+
const cues: SubtitleCue[] = []
|
|
64
|
+
const metadata: any = {}
|
|
65
|
+
|
|
66
|
+
let i = 0
|
|
67
|
+
|
|
68
|
+
// Skip WEBVTT header and metadata
|
|
69
|
+
if (lines[0]?.startsWith('WEBVTT')) {
|
|
70
|
+
i = 1
|
|
71
|
+
// Parse metadata in header
|
|
72
|
+
while (i < lines.length && lines[i].trim() !== '') {
|
|
73
|
+
const line = lines[i].trim()
|
|
74
|
+
if (line.includes(':')) {
|
|
75
|
+
const [key, value] = line.split(':').map(s => s.trim())
|
|
76
|
+
metadata[key.toLowerCase()] = value
|
|
77
|
+
}
|
|
78
|
+
i++
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse cues
|
|
83
|
+
while (i < lines.length) {
|
|
84
|
+
// Skip empty lines
|
|
85
|
+
if (!lines[i]?.trim()) {
|
|
86
|
+
i++
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let cueId: string | undefined
|
|
91
|
+
let timingLine: string
|
|
92
|
+
|
|
93
|
+
// Check if this line is a timing line or cue identifier
|
|
94
|
+
const isTimingLine = lines[i].includes('-->')
|
|
95
|
+
if (isTimingLine) {
|
|
96
|
+
timingLine = lines[i]
|
|
97
|
+
i++
|
|
98
|
+
} else {
|
|
99
|
+
// This is a cue identifier
|
|
100
|
+
cueId = lines[i].trim()
|
|
101
|
+
i++
|
|
102
|
+
timingLine = lines[i] || ''
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parse timing line
|
|
107
|
+
const timing = this.parseVTTTiming(timingLine)
|
|
108
|
+
if (!timing) {
|
|
109
|
+
i++
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Collect cue text
|
|
114
|
+
const textLines: string[] = []
|
|
115
|
+
while (i < lines.length && lines[i]?.trim() !== '') {
|
|
116
|
+
textLines.push(lines[i])
|
|
117
|
+
i++
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (textLines.length > 0) {
|
|
121
|
+
cues.push({
|
|
122
|
+
id: cueId,
|
|
123
|
+
startTime: timing.start,
|
|
124
|
+
endTime: timing.end,
|
|
125
|
+
text: textLines.join('\n'),
|
|
126
|
+
settings: timing.settings
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
i++
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { cues, metadata }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse SRT format
|
|
138
|
+
*/
|
|
139
|
+
private static parseSRT(content: string): ParsedSubtitles {
|
|
140
|
+
const cues: SubtitleCue[] = []
|
|
141
|
+
const blocks = content.split(/\n\s*\n/).filter(block => block.trim())
|
|
142
|
+
|
|
143
|
+
for (const block of blocks) {
|
|
144
|
+
const lines = block.split('\n').map(line => line.trim()).filter(line => line)
|
|
145
|
+
if (lines.length < 3) continue
|
|
146
|
+
|
|
147
|
+
const id = lines[0]
|
|
148
|
+
const timingLine = lines[1]
|
|
149
|
+
const textLines = lines.slice(2)
|
|
150
|
+
|
|
151
|
+
const timing = this.parseSRTTiming(timingLine)
|
|
152
|
+
if (!timing) continue
|
|
153
|
+
|
|
154
|
+
cues.push({
|
|
155
|
+
id,
|
|
156
|
+
startTime: timing.start,
|
|
157
|
+
endTime: timing.end,
|
|
158
|
+
text: textLines.join('\n')
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { cues }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse VTT timing line with settings
|
|
167
|
+
*/
|
|
168
|
+
private static parseVTTTiming(timingLine: string): {
|
|
169
|
+
start: number
|
|
170
|
+
end: number
|
|
171
|
+
settings?: SubtitleCue['settings']
|
|
172
|
+
} | null {
|
|
173
|
+
const parts = timingLine.split(/\s+/)
|
|
174
|
+
const arrowIndex = parts.findIndex(part => part === '-->')
|
|
175
|
+
|
|
176
|
+
if (arrowIndex === -1 || arrowIndex === 0 || arrowIndex === parts.length - 1) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const startTime = this.parseTimeString(parts[arrowIndex - 1])
|
|
181
|
+
const endTime = this.parseTimeString(parts[arrowIndex + 1])
|
|
182
|
+
|
|
183
|
+
if (startTime === null || endTime === null) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Parse cue settings
|
|
188
|
+
const settings: SubtitleCue['settings'] = {}
|
|
189
|
+
const settingsParts = parts.slice(arrowIndex + 2)
|
|
190
|
+
|
|
191
|
+
for (const setting of settingsParts) {
|
|
192
|
+
const [key, value] = setting.split(':')
|
|
193
|
+
if (!key || !value) continue
|
|
194
|
+
|
|
195
|
+
switch (key) {
|
|
196
|
+
case 'line':
|
|
197
|
+
settings.line = value === 'auto' ? 'auto' : parseInt(value)
|
|
198
|
+
break
|
|
199
|
+
case 'position':
|
|
200
|
+
settings.position = parseInt(value)
|
|
201
|
+
break
|
|
202
|
+
case 'size':
|
|
203
|
+
settings.size = parseInt(value)
|
|
204
|
+
break
|
|
205
|
+
case 'align':
|
|
206
|
+
if (['start', 'center', 'end'].includes(value)) {
|
|
207
|
+
settings.align = value as 'start' | 'center' | 'end'
|
|
208
|
+
}
|
|
209
|
+
break
|
|
210
|
+
case 'vertical':
|
|
211
|
+
if (['rl', 'lr'].includes(value)) {
|
|
212
|
+
settings.vertical = value as 'rl' | 'lr'
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
start: startTime,
|
|
220
|
+
end: endTime,
|
|
221
|
+
settings: Object.keys(settings).length > 0 ? settings : undefined
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse SRT timing line
|
|
227
|
+
*/
|
|
228
|
+
private static parseSRTTiming(timingLine: string): {
|
|
229
|
+
start: number
|
|
230
|
+
end: number
|
|
231
|
+
} | null {
|
|
232
|
+
const match = timingLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/)
|
|
233
|
+
if (!match) return null
|
|
234
|
+
|
|
235
|
+
const startTime = this.parseTimeString(match[1].replace(',', '.'))
|
|
236
|
+
const endTime = this.parseTimeString(match[2].replace(',', '.'))
|
|
237
|
+
|
|
238
|
+
if (startTime === null || endTime === null) return null
|
|
239
|
+
|
|
240
|
+
return { start: startTime, end: endTime }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse time string to seconds
|
|
245
|
+
*/
|
|
246
|
+
private static parseTimeString(timeStr: string): number | null {
|
|
247
|
+
const match = timeStr.match(/(\d{2}):(\d{2}):(\d{2})\.(\d{3})/)
|
|
248
|
+
if (!match) return null
|
|
249
|
+
|
|
250
|
+
const hours = parseInt(match[1])
|
|
251
|
+
const minutes = parseInt(match[2])
|
|
252
|
+
const seconds = parseInt(match[3])
|
|
253
|
+
const milliseconds = parseInt(match[4])
|
|
254
|
+
|
|
255
|
+
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Find active cues at a given time
|
|
260
|
+
*/
|
|
261
|
+
static getActiveCues(cues: SubtitleCue[], currentTime: number): SubtitleCue[] {
|
|
262
|
+
return cues.filter(cue =>
|
|
263
|
+
currentTime >= cue.startTime && currentTime <= cue.endTime
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Clean subtitle text (remove HTML tags, etc.)
|
|
269
|
+
*/
|
|
270
|
+
static cleanText(text: string): string {
|
|
271
|
+
return text
|
|
272
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
273
|
+
.replace(/</g, '<')
|
|
274
|
+
.replace(/>/g, '>')
|
|
275
|
+
.replace(/&/g, '&')
|
|
276
|
+
.replace(/"/g, '"')
|
|
277
|
+
.replace(/'/g, "'")
|
|
278
|
+
.trim()
|
|
279
|
+
}
|
|
280
|
+
}
|