1export class VideoPlayerUI { 2 constructor() { 3 this.modal = document.getElementById('video-player-modal'); 4 this.scriptPanel = document.getElementById('video-script-panel'); 5 this.eventListeners = {}; 6 this.buildInitialDOM(); 7 } 8 9 on(event, callback) { 10 if (!this.eventListeners[event]) { 11 this.eventListeners[event] = []; 12 } 13 this.eventListeners[event].push(callback); 14 } 15 16 emit(event, data) { 17 if (this.eventListeners[event]) { 18 this.eventListeners[event].forEach(callback => callback(data)); 19 } 20 } 21 22 buildInitialDOM() { 23 this.modal.innerHTML = ` 24 <div class="video-player-container"> 25 <div class="video-script-selection-container"> 26 <!-- Script selection UI will be injected here --> 27 </div> 28 <div class="video-player-main-area" style="display: none;"> 29 <div class="video-player-content"> 30 <div class="video-player-top-bar"> 31 <h2 class="video-title"></h2> 32 <div class="video-player-top-controls"> 33 <button class="video-script-toggle-btn" title="Ver Guion Completo"><i class="fas fa-file-alt"></i></button> 34 <button class="video-close-btn" title="Cerrar Reproductor"><i class="fas fa-times"></i></button> 35 </div> 36 </div> 37 <div class="teleprompter-container"> 38 <div class="teleprompter-live-indicator"></div> 39 <p class="teleprompter-text"></p> 40 </div> 41 <div class="video-player-bottom-bar"> 42 <div class="video-controls"> 43 <button class="video-play-btn" title="Reproducir"><i class="fas fa-play"></i></button> 44 <button class="video-pause-btn" title="Pausa" style="display: none;"><i class="fas fa-pause"></i></button> 45 <button class="video-stop-btn" title="Detener y Rebobinar"><i class="fas fa-stop"></i></button> 46 </div> 47 <div class="video-progress-container" title="Buscar en el guion"> 48 <div class="video-progress-bar"></div> 49 </div> 50 </div> 51 <div class="video-player-loader"> 52 <div class="loader-spinner"></div> 53 <p class="loader-text"></p> 54 </div> 55 <div class="video-player-error" style="display: none;"> 56 <p class="video-player-error-message"></p> 57 <button class="video-error-exit-btn"><i class="fas fa-times-circle"></i> Salir</button> 58 </div> 59 <div class="video-player-end-screen" style="display: none;"> 60 <h3>¡Guion Terminado!</h3> 61 <div class="end-screen-controls"> 62 <button class="video-replay-btn"><i class="fas fa-redo"></i> Repetir</button> 63 <button class="video-elaborate-btn"><i class="fas fa-rocket"></i> Elaborar</button> 64 <button class="video-exit-btn"><i class="fas fa-times-circle"></i> Salir</button> 65 </div> 66 <div class="video-explore-next-container"> 67 <h4>Explorar Siguiente</h4> 68 <div class="video-explore-next-words"></div> 69 </div> 70 </div> 71 </div> 72 <div class="video-chapters-panel-container"> 73 <div class="video-chapters-header"> 74 <h3>CapÃtulos</h3> 75 </div> 76 <div class="video-chapters-content"> 77 <ol class="video-chapters-list"></ol> 78 </div> 79 </div> 80 </div> 81 </div> 82 `; 83 84 this.scriptPanel.innerHTML = ` 85 <div class="video-script-header"> 86 <h3>Guion Completo</h3>
87 <div class="header-controls"> 88 <button class="video-script-copy-btn" title="Copiar Guion"><i class="fas fa-copy"></i></button> 89 <button class="video-script-close-btn" title="Cerrar Guion"><i class="fas fa-times"></i></button> 90 </div> 91 </div> 92 <div class="video-script-content"> 93 <ul class="video-script-list"></ul> 94 </div> 95 `; 96 97 // Cache DOM elements 98 this.selectionContainer = this.modal.querySelector('.video-script-selection-container'); 99 this.mainPlayerArea = this.modal.querySelector('.video-player-main-area'); 100 101 this.titleEl = this.modal.querySelector('.video-title'); 102 this.teleprompterTextEl = this.modal.querySelector('.teleprompter-text'); 103 this.playBtn = this.modal.querySelector('.video-play-btn'); 104 this.pauseBtn = this.modal.querySelector('.video-pause-btn'); 105 this.stopBtn = this.modal.querySelector('.video-stop-btn'); 106 this.progressContainer = this.modal.querySelector('.video-progress-container'); 107 this.progressBar = this.modal.querySelector('.video-progress-bar'); 108 this.loader = this.modal.querySelector('.video-player-loader'); 109 this.loaderText = this.modal.querySelector('.loader-text'); 110 this.errorEl = this.modal.querySelector('.video-player-error'); 111 this.errorMessageEl = this.errorEl.querySelector('.video-player-error-message'); 112 this.endScreen = this.modal.querySelector('.video-player-end-screen'); 113 this.exploreNextWordsEl = this.modal.querySelector('.video-explore-next-words'); 114 this.scriptListEl = this.scriptPanel.querySelector('.video-script-list'); 115 this.chaptersListEl = this.modal.querySelector('.video-chapters-list'); 116 this.copyBtn = this.scriptPanel.querySelector('.video-script-copy-btn'); 117 118 119 // Attach event listeners 120 this.modal.querySelector('.video-close-btn').addEventListener('click', () => this.emit('close')); 121 this.scriptPanel.querySelector('.video-script-close-btn').addEventListener('click', () => this.closeScriptPanel()); 122 this.modal.querySelector('.video-script-toggle-btn').addEventListener('click', () => this.toggleScriptPanel()); 123 this.playBtn.addEventListener('click', () => this.emit('play')); 124 this.pauseBtn.addEventListener('click', () => this.emit('pause')); 125 this.stopBtn.addEventListener('click', () => this.emit('stop')); 126 this.endScreen.querySelector('.video-replay-btn').addEventListener('click', () => this.emit('replay')); 127 this.endScreen.querySelector('.video-elaborate-btn').addEventListener('click', () => this.emit('elaborate')); 128 this.endScreen.querySelector('.video-exit-btn').addEventListener('click', () => this.emit('close')); 129 this.errorEl.querySelector('.video-error-exit-btn').addEventListener('click', () => this.emit('close')); 130 131 this.copyBtn.addEventListener('click', () => { 132 this.emit('copy'); 133 // Visual feedback for copy 134 const icon = this.copyBtn.querySelector('i'); 135 icon.classList.remove('fa-copy'); 136 icon.classList.add('fa-check'); 137 this.copyBtn.title = "¡Copiado!"; 138 setTimeout(() => { 139 icon.classList.remove('fa-check'); 140 icon.classList.add('fa-copy'); 141 this.copyBtn.title = "Copiar Guion"; 142 }, 2000); 143 }); 144 145 this.progressContainer.addEventListener('click', (e) => { 146 const rect = this.progressContainer.getBoundingClientRect(); 147 const clickX = e.clientX - rect.left; 148 const width = this.progressContainer.clientWidth; 149 const percentage = Math.max(0, Math.min(1, clickX / width)); 150 this.emit('seek', percentage); 151 }); 152 153 this.scriptListEl.addEventListener('click', (e) => { 154 const li = e.target.closest('li[data-index]'); 155 if (li) { 156 const index = parseInt(li.dataset.index, 10); 157 if (!isNaN(index)) {
158 this.emit('jumpToSentence', index); 159 } 160 } 161 }); 162 163 this.chaptersListEl.addEventListener('click', (e) => { 164 const li = e.target.closest('li[data-index]'); 165 if (li) { 166 const index = parseInt(li.dataset.index, 10); 167 if (!isNaN(index)) { 168 this.emit('jumpToChapter', index); 169 } 170 } 171 }); 172 } 173 174 openModal() { 175 this.modal.style.display = 'flex'; 176 document.body.classList.add('video-player-open'); 177 setTimeout(() => this.modal.classList.add('open'), 10); 178 this.resetUI(); 179 } 180 181 closeModal() { 182 this.modal.classList.remove('open'); 183 document.body.classList.remove('video-player-open'); 184 this.closeScriptPanel(); 185 setTimeout(() => { 186 if(this.modal) this.modal.style.display = 'none'; 187 }, 500); 188 } 189 190 toggleScriptPanel() { 191 if (this.scriptPanel.classList.contains('open')) { 192 this.closeScriptPanel(); 193 } else { 194 this.openScriptPanel(); 195 } 196 } 197 198 openScriptPanel() { 199 this.scriptPanel.style.display = 'flex'; 200 setTimeout(() => this.scriptPanel.classList.add('open'), 10); 201 } 202 203 closeScriptPanel() { 204 this.scriptPanel.classList.remove('open'); 205 setTimeout(() => { 206 if(this.scriptPanel) this.scriptPanel.style.display = 'none'; 207 }, 500); 208 } 209 210 toggleChaptersPanel() { 211 if (this.chaptersPanel.classList.contains('open')) { 212 this.closeChaptersPanel(); 213 } else { 214 this.openChaptersPanel(); 215 } 216 } 217 218 openChaptersPanel() { 219 this.closeScriptPanel(); // Close other panel 220 this.chaptersPanel.style.display = 'flex'; 221 setTimeout(() => this.chaptersPanel.classList.add('open'), 10); 222 } 223 224 closeChaptersPanel() { 225 this.chaptersPanel.classList.remove('open'); 226 setTimeout(() => { 227 if(this.chaptersPanel) this.chaptersPanel.style.display = 'none'; 228 }, 500); 229 } 230 231 232 resetUI() { 233 this.titleEl.textContent = ''; 234 this.teleprompterTextEl.innerHTML = '...'; 235 this.progressBar.style.width = '0%'; 236 this.scriptListEl.innerHTML = ''; 237 this.chaptersListEl.innerHTML = ''; 238 this.exploreNextWordsEl.innerHTML = ''; 239 this.loader.style.display = 'none'; 240 this.errorEl.style.display = 'none'; 241 this.endScreen.style.display = 'none'; 242 this.setPlayerState('stopped'); 243 244 this.mainPlayerArea.style.display = 'none'; 245 this.selectionContainer.style.display = 'flex'; 246 this.selectionContainer.innerHTML = ''; 247 } 248 249 showPlayer() { 250 this.selectionContainer.style.display = 'none'; 251 this.mainPlayerArea.style.display = 'flex'; 252 } 253 254 showScriptSelection(word) { 255 this.selectionContainer.innerHTML = ` 256 <button class="close-selection-btn" title="Cerrar"><i class="fas fa-times"></i></button> 257 <h2>Elige un guion para "<span class="selection-word">${word}</span>"</h2> 258 <p>Selecciona el tipo de video generado por IA que te gustarÃa crear.</p> 259 <div class="video-script-options"> 260 <button id="explanation-btn" class="video-script-option-btn" data-script-type="explanation"> 261 <div class="icon-container"><i class="fas fa-lightbulb"></i></div> 262 <h4>Explicación</h4> 263 <p>Un análisis profundo del significado, uso e importancia de la palabra.</p> 264 </button> 265 <button id="comparison-btn" class="video-script-option-btn" data-script-type="comparison"> 266 <div class="icon-container"><i class="fas fa-balance-scale"></i></div> 267 <h4>Comparación</h4> 268 <p>Compara la palabra con sus sinónimos más cercanos para entender los matices.</p> 269 </button> 270 <button id="thematic-btn" class="video-script-option-btn" data-script-type="thematic"> 271 <div class="icon-container"><i class="fas fa-cubes"></i></div> 272 <h4>Temático</h4> 273 <p>Explora una red de ideas, temas y palabras conectadas interesantes.</p> 274 </button> 275 </div> 276 `; 277 278 this.selectionContainer.querySelector('.close-selection-btn').addEventListener('click', () =>
278 this.emit('close')); 279 this.selectionContainer.querySelector('#explanation-btn').addEventListener('click', () => this.emit('scriptSelect', { type: 'explanation' })); 280 this.selectionContainer.querySelector('#comparison-btn').addEventListener('click', () => this.emit('scriptSelect', { type: 'comparison' })); 281 this.selectionContainer.querySelector('#thematic-btn').addEventListener('click', () => this.emit('scriptSelect', { type: 'thematic' })); 282 } 283 284 showComparisonChoice(word) { 285 this.selectionContainer.innerHTML = ` 286 <button class="close-selection-btn" title="Cerrar"><i class="fas fa-times"></i></button> 287 <div class="video-comparison-subflow"> 288 <h3>¿Te gustarÃa elegir con qué palabras comparar <span class="selection-word">${word}</span>?</h3> 289 <p>Si no, la IA elegirá sinónimos interesantes automáticamente.</p> 290 <div class="controls"> 291 <button class="yes-btn" title="SÃ, déjame elegir"><i class="fas fa-check"></i></button> 292 <button class="no-btn" title="No, que elija la IA"><i class="fas fa-times"></i></button> 293 </div> 294 </div> 295 `; 296 this.selectionContainer.querySelector('.close-selection-btn').addEventListener('click', () => this.emit('close')); 297 this.selectionContainer.querySelector('.yes-btn').addEventListener('click', () => this.emit('comparisonChoice', { userSelectsWords: true })); 298 this.selectionContainer.querySelector('.no-btn').addEventListener('click', () => this.emit('comparisonChoice', { userSelectsWords: false })); 299 } 300 301 showComparisonInput(suggestions = []) { 302 this.selectionContainer.innerHTML = ` 303 <button class="close-selection-btn" title="Cerrar"><i class="fas fa-times"></i></button> 304 <div class="video-comparison-subflow"> 305 <h3>¿Con qué palabra(s) la compararás?</h3> 306 <div class="video-comparison-input-area"> 307 <div class="input-group"> 308 <input type="text" class="comparison-input" placeholder="Introduce palabra(s), separadas por comas..."> 309 <button class="submit-comparison-btn" title="Generar Guion"><i class="fas fa-arrow-right"></i></button> 310 </div> 311 <div class="video-comparison-suggestions"> 312 ${suggestions.map(s => `<button class="suggestion-chip" data-word="${s}">${s}</button>`).join('')} 313 </div> 314 </div> 315 </div> 316 `; 317 318 this.selectionContainer.querySelector('.close-selection-btn').addEventListener('click', () => this.emit('close')); 319 320 const input = this.selectionContainer.querySelector('.comparison-input'); 321 const submitBtn = this.selectionContainer.querySelector('.submit-comparison-btn'); 322 323 const handleSubmit = () => { 324 const words = input.value.split(',').map(w => w.trim()).filter(Boolean); 325 if (words.length > 0) { 326 this.emit('comparisonSubmit', { words }); 327 } 328 }; 329 330 submitBtn.addEventListener('click', handleSubmit); 331 input.addEventListener('keyup', (e) => { 332 if (e.key === 'Enter') handleSubmit(); 333 }); 334 335 this.selectionContainer.querySelectorAll('.suggestion-chip').forEach(chip => { 336 chip.addEventListener('click', () => { 337 const word = chip.dataset.word; 338 const currentVal = input.value.trim(); 339 if (currentVal) { 340 input.value = `${currentVal}, ${word}`; 341 } else { 342 input.value = word; 343 } 344 input.focus(); 345 }); 346 }); 347 } 348 349 showLoading(show, text = "Cargando...") { 350 if (show) { 351 this.showPlayer(); 352 } 353 this.loader.style.display = show ? 'flex' : 'none'; 354 if (show) { 355 this.loaderText.textContent = text; 356 } 357 } 358 359 showError(message) { 360 this.showPlayer(); 361 this.errorMessageEl.textContent = message; 362 this.errorEl.style.display = 'flex'; 363 } 364 365 setScript(title, sentences, chapters, exploreNextWords = []) { 366 this.titleEl.textContent = title; 367 this.scriptListEl.innerHTML = sentences.map((s, i) => `<li data-index="${i}">${s}</li>`).join(''); 368 this.chaptersListEl.innerHTML = chapters.map((c, i) => `<li data-index="${i}">${c}</li>`).join(''); 369 this.setExploreNext(exploreNextWords); 370 } 371 372 setExploreNext(words) { 373 this.exploreNextWordsEl.innerHTML = ''; 374 if (!words || words.length === 0) return; 375 376 words.forEach(word => { 377 const wordEl = document.createElement('button'); 378 wordEl.className = 'explore-next-word'; 379 wordEl.textContent = word; 380 wordEl.title = `Explorar "${word}"`; 381 wordEl.addEventListener('click', () => {
382 this.emit('exploreNext', word); 383 }); 384 this.exploreNextWordsEl.appendChild(wordEl); 385 }); 386 } 387 388 updateTeleprompter(html) { 389 this.teleprompterTextEl.classList.remove('visible'); 390 setTimeout(() => { 391 this.teleprompterTextEl.innerHTML = html; 392 this.teleprompterTextEl.classList.add('visible'); 393 }, 150); 394 } 395 396 highlightSentenceInPanel(index) { 397 this.scriptListEl.querySelectorAll('li').forEach(li => li.classList.remove('active')); 398 const activeLi = this.scriptListEl.querySelector(`li[data-index="${index}"]`); 399 if (activeLi) { 400 activeLi.classList.add('active'); 401 activeLi.scrollIntoView({ behavior: 'smooth', block: 'center' }); 402 } 403 } 404 405 highlightChapter(index) { 406 this.chaptersListEl.querySelectorAll('li').forEach(li => li.classList.remove('active')); 407 const activeLi = this.chaptersListEl.querySelector(`li[data-index="${index}"]`); 408 if (activeLi) { 409 activeLi.classList.add('active'); 410 activeLi.scrollIntoView({ behavior: 'smooth', block: 'center' }); 411 } 412 } 413 414 updateProgress(current, total) { 415 const percentage = total > 0 ? (current / total) * 100 : 0; 416 this.progressBar.style.width = `${percentage}%`; 417 } 418 419 setPlayerState(state) { // 'playing', 'paused', 'stopped' 420 this.playBtn.style.display = state === 'playing' ? 'none' : 'block'; 421 this.pauseBtn.style.display = state === 'playing' ? 'block' : 'none'; 422 if(state === 'stopped') { 423 this.updateProgress(0, 1); 424 this.updateTeleprompter('Presiona Reproducir para empezar.'); 425 this.hideEndScreen(); 426 this.highlightChapter(-1); // Deselect all chapters 427 this.highlightSentenceInPanel(-1); // Deselect all sentences 428 } 429 } 430 431 showEndScreen() { 432 this.endScreen.style.display = 'flex'; 433 } 434 435 hideEndScreen() { 436 this.endScreen.style.display = 'none'; 437 } 438}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.