const { invoke } = window.__TAURI__.core; const { getCurrentWindow } = window.__TAURI__.window; // State let stations = []; let currentIndex = 0; let isPlaying = false; let currentMode = 'local'; // 'local' | 'cast' let currentCastDevice = null; const audio = new Audio(); // UI Elements const stationNameEl = document.getElementById('station-name'); const stationSubtitleEl = document.getElementById('station-subtitle'); const statusTextEl = document.getElementById('status-text'); const statusDotEl = document.querySelector('.status-dot'); const playBtn = document.getElementById('play-btn'); const iconPlay = document.getElementById('icon-play'); const iconStop = document.getElementById('icon-stop'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); const volumeSlider = document.getElementById('volume-slider'); const volumeValue = document.getElementById('volume-value'); const castBtn = document.getElementById('cast-toggle-btn'); const castOverlay = document.getElementById('cast-overlay'); const closeOverlayBtn = document.getElementById('close-overlay'); const deviceListEl = document.getElementById('device-list'); const logoTextEl = document.querySelector('.station-logo-text'); const logoImgEl = document.getElementById('station-logo-img'); // Init async function init() { await loadStations(); setupEventListeners(); updateUI(); } async function loadStations() { try { const resp = await fetch('stations.json'); const raw = await resp.json(); // Normalize station objects so the rest of the app can rely on `name` and `url`. stations = raw .map((s) => { // If already in the old format, keep as-is if (s.name && s.url) return s; const name = s.title || s.id || s.name || 'Unknown'; // Prefer liveAudio, fall back to liveVideo or any common fields const url = s.liveAudio || s.liveVideo || s.liveStream || s.url || ''; return { id: s.id || name, name, url, logo: s.logo || s.poster || '', enabled: typeof s.enabled === 'boolean' ? s.enabled : true, raw: s, }; }) // Filter out disabled stations and those without a stream URL .filter((s) => s.enabled !== false && s.url && s.url.length > 0); if (stations.length > 0) { currentIndex = 0; loadStation(currentIndex); } } catch (e) { console.error('Failed to load stations', e); statusTextEl.textContent = 'Error loading stations'; } } function setupEventListeners() { playBtn.addEventListener('click', togglePlay); prevBtn.addEventListener('click', playPrev); nextBtn.addEventListener('click', playNext); volumeSlider.addEventListener('input', handleVolumeInput); castBtn.addEventListener('click', openCastOverlay); closeOverlayBtn.addEventListener('click', closeCastOverlay); // Close overlay on background click castOverlay.addEventListener('click', (e) => { if (e.target === castOverlay) closeCastOverlay(); }); // Close button document.getElementById('close-btn').addEventListener('click', async () => { const appWindow = getCurrentWindow(); await appWindow.close(); }); // Menu button - explicit functionality or placeholder? // For now just log or maybe show about document.getElementById('menu-btn').addEventListener('click', () => { openStationsOverlay(); }); // Hotkeys? } function loadStation(index) { if (index < 0 || index >= stations.length) return; const station = stations[index]; stationNameEl.textContent = station.name; stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream'; // Update Logo Text (First letter or number) // Simple heuristic: if name has a number, use it, else first letter // If station has a logo URL, show the image; otherwise show the text fallback if (station.logo && station.logo.length > 0) { logoImgEl.src = station.logo; logoImgEl.classList.remove('hidden'); logoTextEl.classList.add('hidden'); } else { // Fallback to single-letter/logo text logoImgEl.src = ''; logoImgEl.classList.add('hidden'); const numberMatch = station.name.match(/\d+/); if (numberMatch) { logoTextEl.textContent = numberMatch[0]; } else { logoTextEl.textContent = station.name.charAt(0).toUpperCase(); } logoTextEl.classList.remove('hidden'); } } async function togglePlay() { if (isPlaying) { await stop(); } else { await play(); } } async function play() { const station = stations[currentIndex]; if (!station) return; statusTextEl.textContent = 'Buffering...'; statusDotEl.style.backgroundColor = 'var(--text-muted)'; // Grey/Yellow while loading if (currentMode === 'local') { audio.src = station.url; audio.volume = volumeSlider.value / 100; try { await audio.play(); isPlaying = true; updateUI(); } catch (e) { console.error('Playback failed', e); statusTextEl.textContent = 'Error'; } } else if (currentMode === 'cast' && currentCastDevice) { // Cast logic try { await invoke('cast_play', { deviceName: currentCastDevice, url: station.url }); isPlaying = true; // Sync volume const vol = volumeSlider.value / 100; invoke('cast_set_volume', { deviceName: currentCastDevice, volume: vol }); updateUI(); } catch (e) { console.error('Cast failed', e); statusTextEl.textContent = 'Cast Error'; currentMode = 'local'; // Fallback updateUI(); } } } async function stop() { if (currentMode === 'local') { audio.pause(); audio.src = ''; } else if (currentMode === 'cast' && currentCastDevice) { try { await invoke('cast_stop', { deviceName: currentCastDevice }); } catch (e) { console.error(e); } } isPlaying = false; updateUI(); } async function playNext() { if (stations.length === 0) return; // If playing, stop first? Or seamless? // For radio, seamless switch requires stop then play new URL const wasPlaying = isPlaying; if (wasPlaying) await stop(); currentIndex = (currentIndex + 1) % stations.length; loadStation(currentIndex); if (wasPlaying) await play(); } async function playPrev() { if (stations.length === 0) return; const wasPlaying = isPlaying; if (wasPlaying) await stop(); currentIndex = (currentIndex - 1 + stations.length) % stations.length; loadStation(currentIndex); if (wasPlaying) await play(); } function updateUI() { // Play/Stop Button if (isPlaying) { iconPlay.classList.add('hidden'); iconStop.classList.remove('hidden'); playBtn.classList.add('playing'); // Add pulsing ring animation statusTextEl.textContent = 'Playing'; statusDotEl.style.backgroundColor = 'var(--success)'; stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream'; } else { iconPlay.classList.remove('hidden'); iconStop.classList.add('hidden'); playBtn.classList.remove('playing'); // Remove pulsing ring statusTextEl.textContent = 'Ready'; statusDotEl.style.backgroundColor = 'var(--text-muted)'; stationSubtitleEl.textContent = currentMode === 'cast' ? `Connected to ${currentCastDevice}` : 'Live Stream'; } } function handleVolumeInput() { const val = volumeSlider.value; volumeValue.textContent = `${val}%`; const decimals = val / 100; if (currentMode === 'local') { audio.volume = decimals; } else if (currentMode === 'cast' && currentCastDevice) { invoke('cast_set_volume', { deviceName: currentCastDevice, volume: decimals }); } } // Cast Logic async function openCastOverlay() { castOverlay.classList.remove('hidden'); castOverlay.setAttribute('aria-hidden', 'false'); deviceListEl.innerHTML = '