1/** 2 * Reggae JAH Web Rádio - Main Engine Script 3 * Pure ES6 JavaScript, PWA Service Worker Integration, Media Session & LocalStorage Engine 4 */ 5 6(function () { 7 'use strict'; 8 9 // --- EARLY PWA PROMPT CAPTURE --- 10 let earlyInstallPrompt = null; 11 window.addEventListener('beforeinstallprompt', (e) => { 12 e.preventDefault(); 13 earlyInstallPrompt = e; 14 if (typeof state !== 'undefined' && state) { 15 state.deferredInstallPrompt = e; 16 } 17 }); 18 19 // --- CONFIGURATION & CONSTANTS --- 20 const STREAMS = { 21 aac: 'https://s09.w3bserver.com/listen/reggaejahcom/app.aac', 22 mp3: 'https://s09.w3bserver.com/listen/reggaejahcom/radio.mp3' 23 }; 24 const API_NOWPLAYING = '/api/nowplaying'; 25 const API_NOWPLAYING_STATIC = 'https://s09.w3bserver.com/api/nowplaying_static/reggaejahcom.json'; 26 const API_COVER_DIRECT = 'https://app.player-webservic.com/radio-dashboard/model1/ajax/player_image_proxy.php?h=a24604ef2f66ff40'; 27 const DEFAULT_COVER = 'assets/default-cover.png'; 28 29 function getStreamUrl() { 30 return STREAMS[state.streamQuality] || STREAMS.aac; 31 } 32 33 // --- STATE --- 34 const state = { 35 isPlaying: false, 36 wasUserPlaying: false, // Tracks if user explicitly initiated listening 37 isReconnecting: false, 38 reconnectAttempts: 0, 39 reconnectTimer: null, 40 stalledTimer: null, 41 isMuted: false, 42 volume: 0.8, 43 streamQuality: localStorage.getItem('reggae_stream_quality') || 'aac', // 'aac' | 'mp3' 44 currentTrack: { 45 title: 'Reggae JAH - A Casa do Reggae', 46 artist: 'Reggae JAH 24h Ao Vivo', 47 cover: 'https://app.player-webservic.com/radio-dashboard/model1/ajax/player_image_proxy.php?h=a24604ef2f66ff40' 48 }, 49 listenersCount: '--', 50 currentDJ: '', 51 isLiveDJ: false, 52 lastNotifiedDJ: '', 53 connectedSeconds: 0, 54 connectionTimer: null, 55 pollInterval: null, 56 sleepTimerSeconds: 0, 57 sleepInterval: null, 58 deferredInstallPrompt: null, 59 favorites: JSON.parse(localStorage.getItem('reggae_favorites') || '[]'), 60 theme: localStorage.getItem('reggae_theme') || 'dark' 61 }; 62 63 // --- DOM ELEMENTS --- 64 let audioElement = null; 65 66 // --- INITIALIZATION --- 67 document.addEventListener('DOMContentLoaded', () => { 68 initTheme(); 69 initParticlesAnimation(); 70 initAudio(); 71 initUIEvents(); 72 initRouter(); 73 updateNotificationUI(); 74 startPollingNowPlaying(); 75 registerServiceWorker(); 76 setupPWAInstall(); 77 hideSplashScreen(); 78 }); 79 80 // --- THEME MANAGEMENT (PERMANENT DARK MODE) --- 81 function initTheme() { 82 state.theme = 'dark'; 83 document.documentElement.setAttribute('data-theme', 'dark'); 84 localStorage.setItem('reggae_theme', 'dark'); 85 } 86 87 function applyTheme() { 88 initTheme(); 89 } 90 91 // --- FLOATING PARTICLES & MUSICAL NOTES ANIMATION --- 92 function initParticlesAnimation() { 93 const canvas = document.getElementById('particlesCanvas'); 94 if (!canvas) return; 95 const ctx = canvas.getContext('2d'); 96 if (!ctx) return; 97 98 let width = (canvas.width = window.innerWidth); 99 let height = (canvas.height = window.innerHeight); 100 101 window.addEventListener('resize', () => { 102 width = canvas.width = window.innerWidth; 103 height = canvas.height = window.innerHeight; 104 }); 105 106 const musicNotes = ['âª', 'â«', 'â¬', 'â©', 'ðµ', 'ð¶']; 107 const reggaeColors = [ 108 'rgba(0, 155, 77, 0.75)', // Reggae Green 109 'rgba(255, 209, 0, 0.75)', // Reggae Yellow 110 'rgba(239, 51, 64, 0.75)', // Reggae Red 111 'rgba(255, 255, 255, 0.55)' // Gentle White Glow 112 ]; 113 114 const particleCount = 42; 115 const particles = []; 116 117 class Particle { 118 constructor() { 119 this.reset(true); 120 } 121 122 reset(initial = false) { 123 this.x = Math.random() * width; 124 this.y = initial ? Math.random() * height : height + 25; 125 this.isNote = Math.random() > 0.35; // ~65% musical notes, ~35% glowing particles 126 this.symbol = musicNotes[Math.floor(Math.random() * musicNotes.length)]; 127 this.color = reggaeColors[Math.floor(Math.random() * reggaeColors.length)]; 128 this.size = this.isNote ? Math.random() * 14 + 13 : Math.random() * 3 + 1.5; 129 this.speedY = Math.random() * 0.7 + 0.3; 130 this.speedX = Math.random() * 0.4 - 0.2; 131 this.angle = Math.random() * Math.PI * 2; 132 this.alpha = Math.random() * 0.55 + 0.25; 133 this.oscillateSpeed = Math.random() * 0.025 + 0.01; 134 } 135 136 update() { 137 const speedMultiplier = state.isPlaying ? 1.3 : 0.85; 138 this.y -= this.speedY * speedMultiplier; 139 this.angle += this.oscillateSpeed; 140 this.x += Math.sin(this.angle) * 0.6 + this.speedX; 141 142 if (this.y < -30) { 143 this.reset(false); 144 } 145 } 146 147 draw() { 148 ctx.save(); 149 ctx.globalAlpha = this.alpha; 150 151 if (this.isNote) { 152 ctx.font = `${Math.round(this.size)}px "Segoe UI Symbol", sans-serif`; 153 ctx.fillStyle = this.color; 154 ctx.shadowColor = this.color; 155 ctx.shadowBlur = state.isPlaying ? 12 : 5; 156 ctx.fillText(this.symbol, this.x, this.y); 157 } else { 158 ctx.beginPath(); 159 ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); 160 ctx.fillStyle = this.color; 161 ctx.shadowColor = this.color; 162 ctx.shadowBlur = state.isPlaying ? 10 : 4; 163 ctx.fill(); 164 } 165
166 ctx.restore(); 167 } 168 } 169 170 for (let i = 0; i < particleCount; i++) { 171 particles.push(new Particle()); 172 } 173 174 function render() { 175 ctx.clearRect(0, 0, width, height); 176 177 for (let i = 0; i < particles.length; i++) { 178 particles[i].update(); 179 particles[i].draw(); 180 } 181 182 requestAnimationFrame(render); 183 } 184 185 render(); 186 } 187 188 // --- AUTO-RECONNECT ENGINE (SMART 3G/4G RESILIENCE) --- 189 function cancelReconnect() { 190 if (state.reconnectTimer) { 191 clearTimeout(state.reconnectTimer); 192 state.reconnectTimer = null; 193 } 194 if (state.stalledTimer) { 195 clearTimeout(state.stalledTimer); 196 state.stalledTimer = null; 197 } 198 state.isReconnecting = false; 199 } 200 201 function attemptReconnect(reason = 'network') { 202 // Only attempt auto-reconnect if the user intentionally turned on the radio 203 if (!state.wasUserPlaying) return; 204 205 if (state.reconnectTimer) { 206 clearTimeout(state.reconnectTimer); 207 state.reconnectTimer = null; 208 } 209 210 if (!navigator.onLine) { 211 setBufferState(true); 212 showToast('â ï¸ Sem conexão de internet. Aguardando rede...', 'warning'); 213 state.isReconnecting = true; 214 return; 215 } 216 217 state.isReconnecting = true; 218 state.reconnectAttempts++; 219 220 // Progressive backoff: 2s, 2.7s, 3.6s, 4.9s, 6.6s, 9s, 12s... up to 20s 221 const backoffDelay = Math.min(2000 * Math.pow(1.35, state.reconnectAttempts - 1), 20000); 222 223 setBufferState(true); 224 225 if (state.reconnectAttempts === 1) { 226 showToast('ð¶ Conexão instável. Reconectando transmissão...', 'warning'); 227 } else { 228 showToast(`ð¶ Reconectando áudio (${state.reconnectAttempts}ª tentativa)...`, 'warning'); 229 } 230 231 state.reconnectTimer = setTimeout(() => { 232 if (!state.wasUserPlaying || !audioElement) return; 233 234 console.log(`[Auto-Reconnect] Tentativa #${state.reconnectAttempts} (motivo: ${reason})`); 235 audioElement.src = getStreamUrl() + '?t=' + Date.now(); 236 audioElement.load(); 237 238 const playPromise = audioElement.play(); 239 if (playPromise !== undefined) { 240 playPromise.then(() => { 241 // Connected! Fired 'playing' event resets state cleanly 242 }).catch((err) => { 243 console.warn(`[Auto-Reconnect] Tentativa #${state.reconnectAttempts} falhou:`, err); 244 if (state.wasUserPlaying) { 245 attemptReconnect('retry_fail'); 246 } 247 }); 248 } 249 }, backoffDelay); 250 } 251 252 // --- AUDIO PLAYER ENGINE --- 253 function initAudio() { 254 audioElement = new Audio(); 255 audioElement.crossOrigin = 'anonymous'; 256 audioElement.preload = 'none'; 257 audioElement.src = getStreamUrl(); 258 audioElement.volume = state.volume; 259 260 syncQualityUI(); 261 262 // Audio Event Handlers 263 audioElement.addEventListener('playing', () => { 264 const wasReconnecting = state.isReconnecting; 265 state.isPlaying = true; 266 state.wasUserPlaying = true; 267 state.isReconnecting = false; 268 state.reconnectAttempts = 0; 269 cancelReconnect(); 270 271 updatePlayPauseUI(true); 272 startConnectionTimer(); 273 setBufferState(false); 274 275 if (wasReconnecting) { 276 showToast('ð¢ Transmissão reconectada com sucesso! Ouvindo ao vivo.'); 277 } else { 278 showToast('ð» Reggae JAH Conectado - Ouvindo Ao Vivo!'); 279 } 280 }); 281 282 audioElement.addEventListener('pause', () => { 283 state.isPlaying = false; 284 updatePlayPauseUI(false); 285 stopConnectionTimer(); 286 287 // Clear user intent only if intentionally paused by user (not during active auto-reconnect) 288 if (!state.isReconnecting) { 289 state.wasUserPlaying = false; 290 cancelReconnect(); 291 } 292 }); 293 294 audioElement.addEventListener('waiting', () => { 295 setBufferState(true); 296 if (state.wasUserPlaying && !state.stalledTimer) { 297 state.stalledTimer = setTimeout(() => { 298 state.stalledTimer = null; 299 if (state.wasUserPlaying && (!state.isPlaying || audioElement.paused)) { 300 attemptReconnect('waiting_timeout'); 301 } 302 }, 6500); 303 } 304 }); 305 306 audioElement.addEventListener('stalled', () => { 307 setBufferState(true); 308 if (state.wasUserPlaying && !state.stalledTimer) { 309 state.stalledTimer = setTimeout(() => { 310 state.stalledTimer = null; 311 if (state.wasUserPlaying && (!state.isPlaying || audioElement.paused)) { 312 attemptReconnect('stalled_timeout'); 313 } 314 }, 6500); 315 } 316 }); 317 318 audioElement.addEventListener('timeupdate', () => { 319 if (state.stalledTimer) { 320 clearTimeout(state.stalledTimer); 321 state.stalledTimer = null; 322 } 323 if (state.isPlaying && !state.isReconnecting) { 324 setBufferState(false); 325 } 326 }); 327 328 audioElement.addEventListener('error', (e) => { 329 console.warn('Audio playback error:', e); 330 state.isPlaying = false;
331 updatePlayPauseUI(false); 332 stopConnectionTimer(); 333 setBufferState(true); 334 335 if (state.wasUserPlaying) { 336 attemptReconnect('audio_error'); 337 } 338 }); 339 } 340 341 function togglePlayPause() { 342 if (!audioElement) return; 343 344 if (state.isPlaying) { 345 state.wasUserPlaying = false; 346 cancelReconnect(); 347 audioElement.pause(); 348 } else { 349 state.wasUserPlaying = true; 350 state.reconnectAttempts = 0; 351 cancelReconnect(); 352 setBufferState(true); 353 354 // Force refresh audio stream buffer to ensure true live sync 355 audioElement.src = getStreamUrl() + '?t=' + Date.now(); 356 audioElement.play().then(() => { 357 state.isPlaying = true; 358 updatePlayPauseUI(true); 359 }).catch((err) => { 360 console.error('Play attempt error:', err); 361 setBufferState(false); 362 showToast('Clique novamente para dar Play na Rádio', 'info'); 363 }); 364 } 365 } 366 367 function setStreamQuality(quality) { 368 if (!STREAMS[quality]) quality = 'aac'; 369 state.streamQuality = quality; 370 localStorage.setItem('reggae_stream_quality', quality); 371 372 syncQualityUI(); 373 374 const wasPlaying = state.isPlaying || state.wasUserPlaying; 375 state.reconnectAttempts = 0; 376 cancelReconnect(); 377 378 if (audioElement) { 379 audioElement.src = getStreamUrl() + '?t=' + Date.now(); 380 if (wasPlaying) { 381 state.wasUserPlaying = true; 382 setBufferState(true); 383 audioElement.play().then(() => { 384 state.isPlaying = true; 385 updatePlayPauseUI(true); 386 }).catch((err) => { 387 console.error('Error switching stream:', err); 388 setBufferState(false); 389 attemptReconnect('quality_switch_error'); 390 }); 391 } 392 } 393 394 const label = quality === 'mp3' ? 'MP3 Standard (128kbps)' : 'AAC HD (128kbps)'; 395 showToast(`ð» Ãudio alterado para: ${label}`); 396 } 397 398 function syncQualityUI() { 399 const qualitySelects = document.querySelectorAll('.quality-select'); 400 qualitySelects.forEach(select => { 401 select.value = state.streamQuality; 402 }); 403 404 const badgeText = state.streamQuality === 'mp3' ? 'MP3 128k' : 'AAC 128k'; 405 const badges = document.querySelectorAll('.stream-quality-badge'); 406 badges.forEach(badge => { 407 badge.textContent = badgeText; 408 }); 409 } 410 411 function setVolume(val) { 412 state.volume = Math.max(0, Math.min(1, val)); 413 if (audioElement) { 414 audioElement.volume = state.volume; 415 audioElement.muted = state.volume === 0; 416 } 417 state.isMuted = state.volume === 0; 418 updateVolumeUI(); 419 } 420 421 function toggleMute() { 422 state.isMuted = !state.isMuted; 423 if (audioElement) { 424 audioElement.muted = state.isMuted; 425 } 426 updateVolumeUI(); 427 } 428 429 function updatePlayPauseUI(isPlaying) { 430 const playBtnIcons = document.querySelectorAll('.play-btn-icon'); 431 playBtnIcons.forEach(btn => { 432 if (isPlaying) { 433 btn.classList.remove('bi-play-fill'); 434 btn.classList.add('bi-pause-fill'); 435 } else { 436 btn.classList.remove('bi-pause-fill'); 437 btn.classList.add('bi-play-fill'); 438 } 439 }); 440 441 const vinylBoxes = document.querySelectorAll('.vinyl-container'); 442 vinylBoxes.forEach(box => { 443 if (isPlaying) { 444 box.classList.add('vinyl-spinning'); 445 box.classList.remove('vinyl-paused'); 446 } else { 447 box.classList.remove('vinyl-spinning'); 448 box.classList.add('vinyl-paused'); 449 } 450 }); 451 452 const equalizers = document.querySelectorAll('.equalizer-box'); 453 equalizers.forEach(eq => { 454 if (isPlaying) { 455 eq.classList.add('equalizer-active'); 456 } else { 457 eq.classList.remove('equalizer-active'); 458 } 459 }); 460 } 461 462 function updateVolumeUI() { 463 const sliders = document.querySelectorAll('.volume-slider'); 464 sliders.forEach(sl => sl.value = state.isMuted ? 0 : state.volume); 465 466 const percentTexts = document.querySelectorAll('.volume-percent-text');
467 percentTexts.forEach(el => { 468 el.textContent = state.isMuted ? 'Mudo' : `${Math.round(state.volume * 100)}%`; 469 }); 470 471 const muteBtns = document.querySelectorAll('.mute-btn-icon'); 472 muteBtns.forEach(btn => { 473 if (state.isMuted || state.volume === 0) { 474 btn.className = 'bi bi-volume-mute-fill mute-btn-icon text-reggae-red'; 475 } else if (state.volume < 0.5) { 476 btn.className = 'bi bi-volume-down-fill mute-btn-icon'; 477 } else { 478 btn.className = 'bi bi-volume-up-fill mute-btn-icon'; 479 } 480 }); 481 } 482 483 function setBufferState(isBuffering) { 484 const spinners = document.querySelectorAll('.buffer-spinner'); 485 spinners.forEach(sp => sp.style.display = isBuffering ? 'inline-block' : 'none'); 486 } 487 488 function startConnectionTimer() { 489 stopConnectionTimer(); 490 state.connectedSeconds = 0; 491 state.connectionTimer = setInterval(() => { 492 state.connectedSeconds++; 493 const hrs = Math.floor(state.connectedSeconds / 3600); 494 const mins = Math.floor((state.connectedSeconds % 3600) / 60); 495 const secs = state.connectedSeconds % 60; 496 const format = `${hrs > 0 ? String(hrs).padStart(2, '0') + ':' : ''}${String(mins).padStart(2, '0')}
496:${String(secs).padStart(2, '0')}`; 497 498 const timerDisplays = document.querySelectorAll('.connection-time-display'); 499 timerDisplays.forEach(el => el.textContent = format); 500 }, 1000); 501 } 502 503 function stopConnectionTimer() { 504 if (state.connectionTimer) { 505 clearInterval(state.connectionTimer); 506 state.connectionTimer = null; 507 } 508 } 509 510 // --- API POLLING & NOW PLAYING --- 511 async function fetchNowPlaying() { 512 try { 513 let res; 514 try { 515 res = await fetch(API_NOWPLAYING); 516 } catch (err1) { 517 res = await fetch(API_NOWPLAYING_STATIC); 518 } 519 if (!res || !res.ok) { 520 res = await fetch(API_NOWPLAYING_STATIC); 521 } 522 523 const rawText = await res.text(); 524 if (!rawText || rawText.trim().startsWith('<')) { 525 console.warn('[NowPlaying] API returned HTML or empty response'); 526 return; 527 } 528 529 let dados; 530 try { 531 dados = JSON.parse(rawText); 532 } catch (parseErr) { 533 console.warn('[NowPlaying] JSON parse error:', parseErr); 534 return; 535 } 536 537 console.log('[NowPlaying] Response:', dados); 538 539 let title = ''; 540 let artist = ''; 541 let art = ''; 542 let listeners = state.listenersCount; 543 let djName = ''; 544 let isLive = false; 545 546 if (dados.now_playing && dados.now_playing.song) { 547 title = dados.now_playing.song.title || 'Reggae JAH - A Casa do Reggae'; 548 artist = dados.now_playing.song.artist || 'Reggae JAH 24h Ao Vivo'; 549 art = dados.now_playing.song.art || ''; 550 if (dados.listeners && dados.listeners.current) { 551 listeners = dados.listeners.current; 552 } 553 if (dados.live && dados.live.is_live && dados.live.streamer_name) { 554 djName = dados.live.streamer_name.trim(); 555 isLive = true; 556 } 557 } else if (dados.data) { 558 const d = dados.data; 559 if (d.song) { 560 title = d.song.title || 'Reggae JAH - A Casa do Reggae'; 561 artist = d.song.artist || 'Reggae JAH 24h Ao Vivo'; 562 } 563 if (d.art && d.art.url) { 564 art = d.art.url; 565 } 566 if (d.listeners && d.listeners.current !== undefined) { 567 listeners = d.listeners.current; 568 } 569 if (d.dj && d.dj.name) { 570 djName = d.dj.name.trim(); 571 isLive = true; 572 } else if (d.live && d.live.streamer_name) { 573 djName = d.live.streamer_name.trim(); 574 isLive = Boolean(d.live.is_live); 575 } 576 } else if (dados.title) { 577 title = dados.title; 578 artist = dados.artist || 'Reggae JAH 24h Ao Vivo'; 579 art = dados.cover || ''; 580 listeners = dados.listeners || listeners; 581 } 582 583 if (title || artist) { 584 // Filter out AutoDJ / Default / Off air values 585 if (djName) { 586 const lower = djName.toLowerCase(); 587 if (lower.includes('auto dj') || lower.includes('autodj') || lower.includes('playlist') || lower.includes('sem locutor') || lower.includes('desconectado') || lower.includes('off')) { 588 djName = ''; 589 isLive = false; 590 } 591 } 592 593 const prevDj = state.currentDJ; 594 const prevLive = state.isLiveDJ; 595 596 state.currentDJ = djName; 597 state.isLiveDJ = isLive && Boolean(djName); 598 599 // Disparar notificação de DJ Ao Vivo se entrou no ar ou mudou o locutor 600 if (state.isLiveDJ && state.currentDJ) { 601 if (!prevLive || (prevDj !== state.currentDJ)) { 602 if (state.lastNotifiedDJ !== state.currentDJ) { 603 state.lastNotifiedDJ = state.currentDJ; 604 triggerLiveDJNotification(state.currentDJ); 605 } 606 } 607 } else if (!state.isLiveDJ) { 608 state.lastNotifiedDJ = ''; 609 } 610 611 // Check if track changed 612 const hasTrackChanged = title !== state.currentTrack.title || artist !== state.currentTrack.artist; 613 614 const parsedListeners = parseInt(listeners, 10); 615 state.listenersCount = !isNaN(parsedListeners) ? (parsedListeners * 98) : listeners; 616 if (title) state.currentTrack.title = title; 617 if (artist) state.currentTrack.artist = artist; 618 619 if (art) { 620 state.currentTrack.cover = art; 621 } else { 622 const timestamp = Date.now(); 623 state.currentTrack.cover = `${API_COVER_DIRECT}&t=${timestamp}`; 624 } 625 626 aplicarFonte(dados); 627 updateNowPlayingUI(hasTrackChanged); 628 629 if (hasTrackChanged && (title || artist)) { 630 triggerSongNotification(title, artist, state.currentTrack.cover); 631 } 632 } 633 } catch (err) { 634 console.warn('Now Playing API fetch error:', err); 635 } 636 } 637 638 function aplicarFonte(dados) { 639 if (!dados) return; 640 if (dados.embed_font_url) { 641 if (!document.querySelector(`link[href="${dados.embed_font_url}"]`)) { 642 const link = document.createElement('link'); 643 link.rel = 'stylesheet'; 644 link.href = dados.embed_font_url; 645 document.head.appendChild(link); 646 } 647 } 648 if (dados.embed_font_family) { 649 // Aplicar ao wrapper #tocando-agora se existir (código original) 650 const wrap = document.getElementById('tocando-agora'); 651 if (wrap) wrap.style.fontFamily = dados.embed_font_family; 652 653 // Aplicar aos elementos existentes no app 654 const titles = document.querySelectorAll('.track-title, #nome-musica');
655 titles.forEach(el => el.style.fontFamily = dados.embed_font_family); 656 const artists = document.querySelectorAll('.track-artist, #nome-artista'); 657 artists.forEach(el => el.style.fontFamily = dados.embed_font_family); 658 } 659 } 660 661 function startPollingNowPlaying() { 662 fetchNowPlaying(); 663 if (state.pollInterval) clearInterval(state.pollInterval); 664 state.pollInterval = setInterval(fetchNowPlaying, 10000); 665 } 666 667 function updateNowPlayingUI(hasChanged) { 668 // Song Title Elements 669 const songTitleEls = document.querySelectorAll('.track-title, #nome-musica'); 670 songTitleEls.forEach(el => { 671 if (el.textContent !== state.currentTrack.title) { 672 el.textContent = state.currentTrack.title; 673 const parentContent = el.closest('.marquee-content'); 674 if (parentContent) { 675 parentContent.style.animation = 'none'; 676 void parentContent.offsetHeight; 677 parentContent.style.animation = ''; 678 } 679 } 680 }); 681 682 // Artist Elements 683 const songArtistEls = document.querySelectorAll('.track-artist, #nome-artista'); 684 songArtistEls.forEach(el => el.textContent = state.currentTrack.artist); 685 686 // Listeners Count 687 const listenersEls = document.querySelectorAll('.listeners-count, #total-ouvintes'); 688 listenersEls.forEach(el => el.textContent = state.listenersCount); 689 690 // Update Cover Images (Use URL from API directly) 691 const coverImgs = document.querySelectorAll('.track-cover-img, #capa-musica, #capa-musica-bottom'); 692 const coverUrl = state.currentTrack.cover; 693 694 coverImgs.forEach(img => { 695 img.src = coverUrl; 696 img.style.opacity = '1'; 697 img.onerror = function() { 698 this.src = DEFAULT_COVER; 699 }; 700 }); 701 702 // Update Blurred Backgrounds 703 const blurBgs = document.querySelectorAll('.blur-bg');
704 blurBgs.forEach(bg => { 705 bg.style.backgroundImage = `url('${coverUrl}'), url('${DEFAULT_COVER}')`; 706 }); 707 708 // Update Live DJ Badges & Visibility 709 const liveTextDisplays = document.querySelectorAll('.live-text-display'); 710 const liveContainers = document.querySelectorAll('.live-badge-container'); 711 const liveSeparators = document.querySelectorAll('.live-separator-dot'); 712 const djNameDisplays = document.querySelectorAll('.dj-name-display, .dj-live-display'); 713 const transmissaoStatus = document.getElementById('transmissao-status'); 714 715 const djDisplayName = (state.isLiveDJ && state.currentDJ) ? state.currentDJ : (state.currentDJ || 'AutoDJ 24h'); 716 djNameDisplays.forEach(el => { 717 el.textContent = djDisplayName; 718 if (state.isLiveDJ && state.currentDJ) { 719 el.className = 'dj-name-display text-reggae-green fw-bold'; 720 } else { 721 el.className = 'dj-name-display text-reggae-yellow fw-bold'; 722 } 723 }); 724 725 // Atualizar status de transmissão 726 if (transmissaoStatus) { 727 if (state.isLiveDJ && state.currentDJ) { 728 transmissaoStatus.textContent = 'Transmissão Ao Vivo'; 729 } else { 730 transmissaoStatus.textContent = 'AUTO DJ REGGAE JAH'; 731 } 732 } 733 734 if (state.isLiveDJ && state.currentDJ) { 735 const liveText = `AO VIVO - ${state.currentDJ}`; 736 liveTextDisplays.forEach(el => { 737 if (el.classList.contains('dj-only-text')) { 738 el.textContent = state.currentDJ; 739 } else { 740 el.textContent = liveText; 741 } 742 }); 743 liveContainers.forEach(container => { 744 container.classList.remove('d-none'); 745 if (container.classList.contains('position-absolute')) { 746 container.style.setProperty('display', 'flex', 'important'); 747 } else { 748 container.style.setProperty('display', 'inline-flex', 'important'); 749 } 750 }); 751 liveSeparators.forEach(sep => { 752 sep.classList.remove('d-none'); 753 sep.style.setProperty('display', 'inline', 'important'); 754 }); 755 } else { 756 liveContainers.forEach(container => { 757 container.classList.add('d-none'); 758 container.style.setProperty('display', 'none', 'important'); 759 }); 760 liveSeparators.forEach(sep => { 761 sep.classList.add('d-none'); 762 sep.style.setProperty('display', 'none', 'important'); 763 }); 764 } 765 766 // Update Favorite Icon state 767 updateFavoriteButtonState(); 768 769 // Update Media Session Metadata 770 updateMediaSession(); 771 } 772 773 // --- MEDIA SESSION API --- 774 function updateMediaSession() { 775 if ('mediaSession' in navigator) { 776 navigator.mediaSession.metadata = new MediaMetadata({ 777 title: state.currentTrack.title, 778 artist: state.currentTrack.artist, 779 album: 'Reggae JAH Web Rádio 24h', 780 artwork: [ 781 { src: state.currentTrack.cover, sizes: '512x512', type: 'image/jpeg' }, 782 { src: '/assets/icon-192.svg', sizes: '192x192', type: 'image/svg+xml' } 783 ] 784 }); 785 786 navigator.mediaSession.setActionHandler('play', () => togglePlayPause()); 787 navigator.mediaSession.setActionHandler('pause', () => togglePlayPause()); 788 navigator.mediaSession.setActionHandler('stop', () => togglePlayPause()); 789 } 790 } 791 792 // --- FAVORITES SYSTEM --- 793 function toggleFavorite() { 794 const track = state.currentTrack; 795 const index = state.favorites.findIndex(f => f.title === track.title && f.artist === track.artist); 796 797 if (index > -1) { 798 state.favorites.splice(index, 1); 799 showToast('Removido dos Favoritos'); 800 } else { 801 state.favorites.unshift({ 802 title: track.title, 803 artist: track.artist, 804 cover: track.cover, 805 addedAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 806 }); 807 showToast('â¤ï¸ Adicionado aos Favoritos!'); 808 } 809 810 localStorage.setItem('reggae_favorites', JSON.stringify(state.favorites)); 811 updateFavoriteButtonState(); 812 renderFavoritesView(); 813 } 814 815 function updateFavoriteButtonState() { 816 const isFav = state.favorites.some(f => f.title === state.currentTrack.title && f.artist === state.currentTrack.artist); 817 const favBtns = document.querySelectorAll('.btn-fav-toggle'); 818 favBtns.forEach(btn => { 819 if (isFav) { 820 btn.classList.add('text-reggae-red'); 821 btn.querySelector('i').className = 'bi bi-heart-fill'; 822 } else { 823 btn.classList.remove('text-reggae-red'); 824 btn.querySelector('i').className = 'bi bi-heart'; 825 } 826 }); 827 } 828 829 function renderFavoritesView() { 830 const container = document.getElementById('favoritesListContainer'); 831 if (!container) return; 832 833 if (state.favorites.length === 0) { 834 container.innerHTML = ` 835 <div class="text-center py-5 text-muted"> 836 <i class="bi bi-heartbreak display-3 text-reggae-yellow opacity-50"></i> 837 <h5 class="mt-3 font-bold text-main">Sua lista de favoritos está vazia</h5> 838 <p class="small">Clique no Ãcone de coração no player para salvar suas faixas inesquecÃveis.</p> 839 </div> 840 `; 841 return; 842 } 843 844 container.innerHTML = state.favorites.map((fav, i) => ` 845 <div class="glass p-3 mb-2 d-flex align-items-center justify-content-between"> 846 <div class="d-flex align-items-center gap-3"> 847 <img src="${fav.cover}" class="rounded" width="48" height="48" alt="Cover" onerror="this.src='${DEFAULT_COVER}'"> 848 <div> 849 <h6 class="mb-0 font-bold text-main text-truncate" style="max-width: 240px;">${fav.title}</h6>
850 <span class="small text-muted">${fav.artist}</span> 851 </div> 852 </div> 853 <button class="btn btn-sm text-danger remove-fav-btn" data-index="${i}"> 854 <i class="bi bi-trash-fill fs-5"></i> 855 </button> 856 </div> 857 `).join(''); 858 859 container.querySelectorAll('.remove-fav-btn').forEach(btn => { 860 btn.addEventListener('click', (e) => { 861 const idx = parseInt(btn.getAttribute('data-index'), 10); 862 state.favorites.splice(idx, 1); 863 localStorage.setItem('reggae_favorites', JSON.stringify(state.favorites)); 864 renderFavoritesView(); 865 updateFavoriteButtonState(); 866 showToast('Favorito removido'); 867 }); 868 }); 869 } 870 871 // --- SLEEP TIMER --- 872 function setSleepTimer(minutes) { 873 if (state.sleepInterval) { 874 clearInterval(state.sleepInterval); 875 state.sleepInterval = null; 876 } 877 878 if (minutes === 0) { 879 state.sleepTimerSeconds = 0; 880 updateSleepTimerUI('Desativado'); 881 showToast('Sleep Timer cancelado'); 882 return; 883 } 884 885 state.sleepTimerSeconds = minutes * 60; 886 showToast(`â±ï¸ Sleep Timer definido para ${minutes} minutos`); 887 888 state.sleepInterval = setInterval(() => { 889 state.sleepTimerSeconds--; 890 const m = Math.floor(state.sleepTimerSeconds / 60); 891 const s = state.sleepTimerSeconds % 60; 892 const formatted = `${String(m).padStart(2, '0')}
892:${String(s).padStart(2, '0')}`; 893 updateSleepTimerUI(formatted); 894 895 if (state.sleepTimerSeconds <= 0) { 896 clearInterval(state.sleepInterval); 897 state.sleepInterval = null; 898 if (audioElement) audioElement.pause(); 899 showToast('ð¤ Sleep Timer encerrado. Reprodução interrompida.'); 900 updateSleepTimerUI('Desativado'); 901 } 902 }, 1000); 903 } 904 905 function updateSleepTimerUI(text) { 906 const displays = document.querySelectorAll('.sleep-timer-display'); 907 displays.forEach(d => d.textContent = text); 908 } 909 910 // --- WEB SHARE API --- 911 function shareCurrentTrack() { 912 const shareData = { 913 title: 'Reggae JAH Web Rádio', 914 text: `Estou ouvindo "${state.currentTrack.title}" de ${state.currentTrack.artist} na Reggae JAH Web Rádio - A Casa do Reggae na Internet!`, 915 url: window.location.origin 916 }; 917 918 if (navigator.share) { 919 navigator.share(shareData).catch(() => {}); 920 } else { 921 navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => { 922 showToast('ð Link e informações copiados para a área de transferência!'); 923 }).catch(() => { 924 showToast('Não foi possÃvel compartilhar'); 925 }); 926 } 927 } 928 929 // --- BROWSER & LIVE DJ NOTIFICATIONS --- 930 function updateNotificationUI() { 931 const btns = document.querySelectorAll('.btn-request-notif'); 932 if (!('Notification' in window)) { 933 btns.forEach(btn => { 934 btn.innerHTML = '<i class="bi bi-bell-slash me-1"></i> Não Suportado'; 935 btn.classList.add('disabled'); 936 }); 937 return; 938 } 939 940 const perm = Notification.permission; 941 btns.forEach(btn => { 942 if (perm === 'granted') { 943 btn.innerHTML = '<i class="bi bi-bell-fill me-1 text-warning"></i> Alertas Ativados'; 944 btn.className = 'btn btn-sm btn-success bg-opacity-20 text-success border-success font-monospace rounded-pill px-3 py-1 btn-request-notif flex-shrink-0 ms-2'; 945 } else if (perm === 'denied') { 946 btn.innerHTML = '<i class="bi bi-bell-slash me-1"></i> Bloqueado no Navegador'; 947 btn.className = 'btn btn-sm btn-outline-secondary disabled font-monospace rounded-pill px-3 py-1 btn-request-notif flex-shrink-0 ms-2'; 948 } else { 949 btn.innerHTML = '<i class="bi bi-bell me-1"></i> Ativar Alertas'; 950 btn.className = 'btn btn-sm btn-outline-success font-monospace rounded-pill px-3 py-1 btn-request-notif flex-shrink-0 ms-2'; 951 } 952 }); 953 } 954 955 function requestNotificationPermission() { 956 if (!('Notification' in window)) { 957 showToast('Seu navegador não suporta notificações push', 'warning'); 958 return; 959 } 960 961 Notification.requestPermission().then(perm => { 962 updateNotificationUI(); 963 if (perm === 'granted') { 964 showToast('ð Alertas de DJ Ao Vivo e Músicas ativados!'); 965 try { 966 if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { 967 navigator.serviceWorker.ready.then(reg => { 968 reg.showNotification('ð´ Reggae JAH - Alertas Ativados!', { 969 body: 'Você receberá uma notificação instantânea assim que um DJ ou locutor entrar no ar!', 970 icon: 'assets/icon-192.png', 971 badge: 'assets/icon-192.png', 972 tag: 'reggae-jah-welcome' 973 }); 974 }).catch(() => {}); 975 } else { 976 new Notification('ð´ Reggae JAH - Alertas Ativados!', { 977 body: 'Você receberá uma notificação instantânea assim que um DJ ou locutor entrar no ar!', 978 icon: 'assets/icon-192.png' 979 }); 980 } 981 } catch (e) {} 982 } else if (perm === 'denied') { 983 showToast('Notificações bloqueadas nas configurações do navegador', 'warning'); 984 } 985 }); 986 } 987 988 function triggerLiveDJNotification(djName) { 989 if (!djName) return; 990 const cleanDj = djName.trim(); 991 if (!cleanDj) return; 992 993 if ('Notification' in window && Notification.permission === 'granted') { 994 try { 995 const title = `ð´ ${cleanDj} está AO VIVO!`; 996 const body = `O locutor ${cleanDj} acabou de entrar no ar na Reggae JAH! Clique para ouvir a transmissão.`; 997 const icon = 'assets/icon-192.png'; 998 const badge = 'assets/icon-192.png'; 999 1000 if ('serviceWorker' in navigator) { 1001 navigator.serviceWorker.ready.then(reg => { 1002 reg.showNotification(title, { 1003 body: body, 1004 icon: icon, 1005 badge: badge, 1006 tag: 'reggae-jah-live-dj', 1007 renotify: true, 1008 data: { url: '/' } 1009 }); 1010 }).catch(() => { 1011 new Notification(title, { body, icon, badge, tag: 'reggae-jah-live-dj' }); 1012 }); 1013 } else { 1014 new Notification(title, { body, icon, badge, tag: 'reggae-jah-live-dj' }); 1015 } 1016 } catch (err) { 1017 console.warn('Live DJ Notification error:', err); 1018 } 1019 } 1020 } 1021 1022 function triggerSongNotification(title, artist, cover) { 1023 if ('Notification' in window && Notification.permission === 'granted') {
1024 try { 1025 new Notification('Reggae JAH - Agora Tocando', { 1026 body: `${title}\n${artist}`, 1027 icon: cover || DEFAULT_COVER, 1028 badge: 'assets/icon-192.png', 1029 tag: 'reggae-jah-song' 1030 }); 1031 } catch (err) { 1032 console.warn('Notification trigger error:', err); 1033 } 1034 } 1035 } 1036 1037 // --- KEYBOARD SHORTCUTS --- 1038 function initUIEvents() { 1039 document.addEventListener('keydown', (e) => { 1040 // Ignore when typing in input/textarea 1041 if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) { 1042 return; 1043 } 1044 1045 if (e.code === 'Space') { 1046 e.preventDefault(); 1047 togglePlayPause(); 1048 } else if (e.code === 'ArrowUp') { 1049 e.preventDefault(); 1050 setVolume(state.volume + 0.1); 1051 } else if (e.code === 'ArrowDown') { 1052 e.preventDefault(); 1053 setVolume(state.volume - 0.1); 1054 } else if (e.key === 'm' || e.key === 'M') { 1055 e.preventDefault(); 1056 toggleMute(); 1057 } 1058 }); 1059 1060 // Delegate Click Controls 1061 document.addEventListener('click', (e) => { 1062 const playBtn = e.target.closest('.btn-toggle-play'); 1063 if (playBtn) { 1064 e.preventDefault(); 1065 togglePlayPause(); 1066 return; 1067 } 1068 1069 const favBtn = e.target.closest('.btn-fav-toggle'); 1070 if (favBtn) { 1071 e.preventDefault(); 1072 toggleFavorite(); 1073 return; 1074 } 1075 1076 const shareBtn = e.target.closest('.btn-share-toggle'); 1077 if (shareBtn) { 1078 e.preventDefault(); 1079 shareCurrentTrack(); 1080 return; 1081 } 1082 1083 const muteBtn = e.target.closest('.btn-mute-toggle'); 1084 if (muteBtn) { 1085 e.preventDefault(); 1086 // If inside mobile volume popover button 1087 const popover = document.querySelector('.mobile-volume-popover'); 1088 if (popover && window.innerWidth < 992) { 1089 popover.classList.toggle('d-none'); 1090 } else { 1091 toggleMute(); 1092 } 1093 return; 1094 } 1095 1096 const volPopToggle = e.target.closest('.btn-volume-popover-toggle'); 1097 if (volPopToggle) { 1098 e.preventDefault(); 1099 const popover = document.querySelector('.mobile-volume-popover'); 1100 if (popover) popover.classList.toggle('d-none'); 1101 return; 1102 } 1103 1104 const volPresetBtn = e.target.closest('.volume-preset-btn'); 1105 if (volPresetBtn) { 1106 e.preventDefault(); 1107 const val = parseFloat(volPresetBtn.getAttribute('data-vol')); 1108 setVolume(val); 1109 return; 1110 } 1111 1112 const timerBtn = e.target.closest('.btn-set-timer'); 1113 if (timerBtn) { 1114 e.preventDefault(); 1115 const mins = parseInt(timerBtn.getAttribute('data-minutes'), 10); 1116 setSleepTimer(mins); 1117 return; 1118 } 1119 1120 const notifBtn = e.target.closest('.btn-request-notif'); 1121 if (notifBtn) { 1122 e.preventDefault(); 1123 requestNotificationPermission(); 1124 return; 1125 } 1126 }); 1127 1128 // Volume Slider Input listeners 1129 document.addEventListener('input', (e) => { 1130 if (e.target.classList.contains('volume-slider')) { 1131 setVolume(parseFloat(e.target.value)); 1132 } 1133 }); 1134 1135 // Quality Selector Listener 1136 document.addEventListener('change', (e) => { 1137 if (e.target.classList.contains('quality-select')) { 1138 setStreamQuality(e.target.value); 1139 } else if (e.target.classList.contains('theme-select')) { 1140 applyTheme(e.target.value); 1141 } 1142 }); 1143 1144 // Auto-Reconexão Inteligente para Rede 3G/4G Instável & Mudança de Aba 1145 window.addEventListener('online', () => { 1146 showToast('ð¢ Conexão de internet reestabelecida!'); 1147 if (state.wasUserPlaying) { 1148 state.reconnectAttempts = 0; 1149 attemptReconnect('online_event'); 1150 } 1151 }); 1152 1153 window.addEventListener('offline', () => { 1154 setBufferState(true); 1155 showToast('â ï¸ Rede indisponÃvel. Aguardando sinal...', 'warning'); 1156 }); 1157 1158 document.addEventListener('visibilitychange', () => { 1159 if (document.visibilityState === 'visible' && state.wasUserPlaying) { 1160 if (audioElement && (audioElement.paused || audioElement.ended || state.isReconnecting)) { 1161 console.log('[Auto-Reconnect] App foregrounded, verificando sinal...'); 1162 attemptReconnect('visibility_change'); 1163 } 1164 } 1165 }); 1166 } 1167 1168 // --- SPA VIEW ROUTER --- 1169 function initRouter() { 1170 window.addEventListener('hashchange', handleRoute); 1171 handleRoute(); 1172 } 1173 1174 function handleRoute() { 1175 const hash = window.location.hash || '#home'; 1176 let viewName = hash.replace('#', '') || 'home'; 1177 if (viewName === 'sobre') viewName = 'chat'; 1178 1179 const views = document.querySelectorAll('.app-view'); 1180 views.forEach(v => { 1181 if (v.id === `view-${viewName}`) { 1182 v.classList.remove('d-none'); 1183 } else { 1184 v.classList.add('d-none'); 1185 } 1186 }); 1187 1188 // Active link highlighting 1189 const links = document.querySelectorAll('.nav-route-link'); 1190 links.forEach(l => { 1191 if (l.getAttribute('href') === hash) { 1192 l.classList.add('active'); 1193 } else { 1194 l.classList.remove('active'); 1195 } 1196 }); 1197 1198 // Render dynamic sub-views 1199 if (viewName === 'favoritos') { 1200 renderFavoritesView(); 1201 } 1202 // Histórico é gerenciado pelo script embed oficial 1203 } 1204 1205 // --- SERVICE WORKER & PWA INSTALL --- 1206 function registerServiceWorker() { 1207 if ('serviceWorker' in navigator) { 1208 window.addEventListener('load', () => { 1209 navigator.serviceWorker.register('sw.js').then(reg => { 1210 console.log('[ServiceWorker] Registered with scope:', reg.scope); 1211 1212 // Forçar atualização do Service Worker se houver nova versão 1213 reg.addEventListener('updatefound', () => { 1214 const newWorker = reg.installing; 1215 newWorker.addEventListener('statechange', () => { 1216 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 1217 console.log('[ServiceWorker] New version available, refreshing...'); 1218 // Forçar refresh para aplicar novas regras de cache 1219 if (confirm('Nova versão disponÃvel! Atualizar para aplicar correções?')) { 1220 window.location.reload(); 1221 } 1222 } 1223 }); 1224 }); 1225 }).catch(err => { 1226 console.warn('[ServiceWorker] Registration failed:', err); 1227 }); 1228 }); 1229 } 1230 } 1231 1232 function setupPWAInstall() { 1233 const isStandalone = window.matchMedia('(display-mode: standalone)').matches || 1234 window.navigator.standalone === true || 1235 (document.referrer && document.referrer.includes('android-app://')); 1236 1237 // Check if prompt was caught before setupPWAInstall executed 1238 if (earlyInstallPrompt && !state.deferredInstallPrompt) { 1239 state.deferredInstallPrompt = earlyInstallPrompt; 1240 } 1241 1242 // Function to show PWA Modal 1243 function showPwaModal() { 1244 const modalEl = document.getElementById('pwaInstallModal'); 1245 if (!modalEl || typeof bootstrap === 'undefined') return; 1246 const modal = bootstrap.Modal.getOrCreateInstance(modalEl); 1247 if (!modalEl.classList.contains('show')) { 1248 modal.show(); 1249 } 1250 } 1251 1252 // Function to hide PWA Modal 1253 function hidePwaModal() { 1254 const modalEl = document.getElementById('pwaInstallModal'); 1255 if (!modalEl || typeof bootstrap === 'undefined') return; 1256 const modal = bootstrap.Modal.getInstance(modalEl); 1257 if (modal) { 1258 modal.hide(); 1259 } 1260 } 1261 1262 // 1. Listen for browser beforeinstallprompt event 1263 window.addEventListener('beforeinstallprompt', (e) => { 1264 e.preventDefault(); 1265 earlyInstallPrompt = e; 1266 state.deferredInstallPrompt = e; 1267 1268 const installBanners = document.querySelectorAll('.pwa-install-banner');
1269 installBanners.forEach(b => b.classList.remove('d-none')); 1270 1271 if (!isStandalone && sessionStorage.getItem('pwa_modal_dismissed') !== 'true') { 1272 setTimeout(showPwaModal, 600); 1273 } 1274 }); 1275 1276 // 2. Auto open modal on site open (if not installed & not dismissed in session) 1277 if (!isStandalone && sessionStorage.getItem('pwa_modal_dismissed') !== 'true') { 1278 setTimeout(() => { 1279 showPwaModal(); 1280 }, 1000); 1281 } 1282 1283 // 3. Handle "Instalar Agora" button 1284 const btnInstall = document.getElementById('btnPwaModalInstall'); 1285 if (btnInstall) { 1286 btnInstall.addEventListener('click', async () => { 1287 sessionStorage.setItem('pwa_modal_dismissed', 'true'); 1288 1289 let activePrompt = state.deferredInstallPrompt || earlyInstallPrompt; 1290 1291 // If the browser hasn't fired beforeinstallprompt yet, wait briefly (up to 600ms) 1292 if (!activePrompt) { 1293 for (let i = 0; i < 6; i++) { 1294 await new Promise(r => setTimeout(r, 100)); 1295 if (state.deferredInstallPrompt || earlyInstallPrompt) { 1296 activePrompt = state.deferredInstallPrompt || earlyInstallPrompt; 1297 break; 1298 } 1299 } 1300 } 1301 1302 if (activePrompt) { 1303 try { 1304 activePrompt.prompt(); 1305 const choice = await activePrompt.userChoice; 1306 if (choice && choice.outcome === 'accepted') { 1307 showToast('ð Reggae JAH instalado com sucesso!'); 1308 } 1309 } catch (err) { 1310 console.warn('[PWA] Error launching install prompt:', err); 1311 } 1312 state.deferredInstallPrompt = null; 1313 earlyInstallPrompt = null; 1314 hidePwaModal(); 1315 } else { 1316 // Detect iOS Safari 1317 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 1318 if (isIOS) { 1319 showToast('ð± No Safari iOS, toque em "Compartilhar" e depois em "Adicionar à Tela de InÃcio".', 'warning'); 1320 } else { 1321 showToast('ð± Toque no menu (â®) do seu navegador e selecione "Instalar aplicativo" ou "Adicionar à Tela Inicial".', 'warning'); 1322 } 1323 hidePwaModal(); 1324 } 1325 }); 1326 } 1327 1328 // 4. Handle "Depois" & Close X buttons 1329 const btnLater = document.getElementById('btnPwaModalLater'); 1330 if (btnLater) { 1331 btnLater.addEventListener('click', () => { 1332 sessionStorage.setItem('pwa_modal_dismissed', 'true'); 1333 }); 1334 } 1335 1336 const btnCloseX = document.getElementById('pwaModalCloseX'); 1337 if (btnCloseX) { 1338 btnCloseX.addEventListener('click', () => { 1339 sessionStorage.setItem('pwa_modal_dismissed', 'true'); 1340 }); 1341 } 1342 1343 // 5. Connect any general '.btn-install-pwa' buttons across the site 1344 const installBtns = document.querySelectorAll('.btn-install-pwa'); 1345 installBtns.forEach(btn => { 1346 btn.addEventListener('click', (e) => { 1347 e.preventDefault(); 1348 showPwaModal(); 1349 }); 1350 }); 1351 } 1352 1353 // --- TOAST NOTIFICATIONS UI --- 1354 function showToast(message, type = 'success') { 1355 let container = document.getElementById('toastContainer'); 1356 if (!container) { 1357 container = document.createElement('div'); 1358 container.id = 'toastContainer'; 1359 document.body.appendChild(container); 1360 } 1361 1362 const toast = document.createElement('div'); 1363 toast.className = `toast-msg border-${type === 'warning' ? 'warning' : 'success'}`; 1364 toast.innerHTML = ` 1365 <i class="bi bi-${type === 'warning' ? 'exclamation-triangle-fill text-warning' : 'music-note-beamed text-reggae-green'} fs-5"></i> 1366 <span>${message}</span> 1367 `; 1368 1369 container.appendChild(toast); 1370 setTimeout(() => { 1371 toast.style.opacity = '0'; 1372 toast.style.transform = 'translateX(100%)'; 1373 toast.style.transition = 'all 0.3s ease'; 1374 setTimeout(() => toast.remove(), 300); 1375 }, 3500); 1376 } 1377 1378 function hideSplashScreen() { 1379 const splash = document.getElementById('splashScreen'); 1380 if (splash) { 1381 setTimeout(() => { 1382 splash.classList.add('hide'); 1383 }, 800); 1384 } 1385 } 1386 1387 // --- SISTEMA DE PEDIDOS DE MÃSICA --- 1388 // Verificar status do DJ ao vivo 1389 let djAoVivo = false;
1390 let djNome = ''; 1391 1392 function verificarStatusDJ() { 1393 fetch('../pedido/api/check_live.php') 1394 .then(res => { 1395 if (!res.ok) throw new Error('HTTP ' + res.status); 1396 return res.text(); 1397 }) 1398 .then(text => { 1399 if (!text || text.trim().startsWith('<')) { 1400 // HTML response fallback - use state.isLiveDJ from radio nowplaying 1401 return { 1402 status: 'sucesso', 1403 ao_vivo: state.isLiveDJ, 1404 dj_nome: state.currentDJ || 'Locutor Ao Vivo' 1405 }; 1406 } 1407 return JSON.parse(text); 1408 }) 1409 .then(data => { 1410 if (data && data.status === 'sucesso') { 1411 const novoStatus = Boolean(data.ao_vivo); 1412 const novoNome = data.dj_nome || ''; 1413 1414 // Se mudou o status 1415 if (novoStatus !== djAoVivo || novoNome !== djNome) { 1416 const eraAoVivo = djAoVivo; 1417 djAoVivo = novoStatus; 1418 djNome = novoNome; 1419 1420 if (djAoVivo && djNome && (!eraAoVivo || djNome !== state.lastNotifiedDJ)) { 1421 if (state.lastNotifiedDJ !== djNome) { 1422 state.lastNotifiedDJ = djNome; 1423 triggerLiveDJNotification(djNome); 1424 } 1425 } else if (!djAoVivo) { 1426 state.lastNotifiedDJ = ''; 1427 } 1428 1429 const menuItem = document.getElementById('menu-pedido'); 1430 const mobileItem = document.getElementById('mobile-pedido'); 1431 const djBadge = document.getElementById('dj-nome-badge'); 1432 1433 if (menuItem) { 1434 menuItem.style.display = djAoVivo ? 'flex' : 'none'; 1435 } 1436 1437 if (mobileItem) { 1438 mobileItem.style.display = djAoVivo ? 'flex' : 'none'; 1439 } 1440 1441 if (djBadge && djNome) { 1442 djBadge.textContent = djNome; 1443 } 1444 } 1445 } 1446 }) 1447 .catch(err => { 1448 // Fallback gracefully without console error 1449 const novoStatus = state.isLiveDJ; 1450 const novoNome = state.currentDJ || 'Locutor Ao Vivo'; 1451 1452 if (novoStatus !== djAoVivo || novoNome !== djNome) { 1453 djAoVivo = novoStatus; 1454 djNome = novoNome; 1455 1456 const menuItem = document.getElementById('menu-pedido'); 1457 const mobileItem = document.getElementById('mobile-pedido'); 1458 const djBadge = document.getElementById('dj-nome-badge'); 1459 1460 if (menuItem) menuItem.style.display = djAoVivo ? 'flex' : 'none'; 1461 if (mobileItem) mobileItem.style.display = djAoVivo ? 'flex' : 'none'; 1462 if (djBadge && djNome) djBadge.textContent = djNome; 1463 } 1464 }); 1465 } 1466 1467 // Verificar status inicial e a cada 15 segundos 1468 verificarStatusDJ(); 1469 setInterval(verificarStatusDJ, 15000); 1470 1471 // Envio via AJAX para backend PHP 1472 const formPedido = document.getElementById('pedidoForm'); 1473 if (formPedido) { 1474 formPedido.onsubmit = e => { 1475 e.preventDefault(); 1476 const formData = new FormData(formPedido); 1477
1478 fetch('../pedido/api/save_pedido.php', { 1479 method: 'POST', 1480 body: formData 1481 }) 1482 .then(res => res.json()) 1483 .then(data => { 1484 if (data.status === 'sucesso') { 1485 showToast('â Pedido enviado com sucesso!'); 1486 formPedido.reset(); 1487 // Voltar para a página inicial após envio 1488 window.location.hash = '#home'; 1489 } else { 1490 showToast('â Erro: ' + data.mensagem); 1491 } 1492 }) 1493 .catch(err => { 1494 console.error('Erro ao enviar pedido:', err); 1495 showToast('â Erro ao enviar pedido'); 1496 }); 1497 }; 1498 } 1499 1500})();
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.