PageSourceSearch

https://www.reggaejah.com/radio-reggae-jah/script.js

js reggaejah.com collected 2026-10-08 12:18:16 UTC 50,384 bytes, 1,500 lines download raw bytes

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.