Initial commit

This commit is contained in:
2025-12-30 15:12:26 +01:00
commit 5934d24f7f
38 changed files with 7636 additions and 0 deletions

277
src/main.js Normal file
View File

@@ -0,0 +1,277 @@
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');
// Init
async function init() {
await loadStations();
setupEventListeners();
updateUI();
}
async function loadStations() {
try {
const resp = await fetch('stations.json');
stations = await resp.json();
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 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', () => {
// Future: Settings menu
console.log('Menu clicked');
});
// 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
const numberMatch = station.name.match(/\d+/);
if (numberMatch) {
logoTextEl.textContent = numberMatch[0];
} else {
logoTextEl.textContent = station.name.charAt(0).toUpperCase();
}
}
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');
deviceListEl.innerHTML = '<li>Scanning...</li>';
try {
const devices = await invoke('list_cast_devices');
deviceListEl.innerHTML = '';
// Add "Stop Casting / Local" option
const localLi = document.createElement('li');
localLi.textContent = 'This Computer (Local Playback)';
localLi.onclick = () => selectCastDevice(null);
deviceListEl.appendChild(localLi);
if (devices.length === 0) {
const li = document.createElement('li');
li.textContent = 'No speakers found';
deviceListEl.appendChild(li);
} else {
devices.forEach(d => {
const li = document.createElement('li');
li.textContent = d;
li.onclick = () => selectCastDevice(d);
deviceListEl.appendChild(li);
});
}
} catch (e) {
deviceListEl.innerHTML = `<li>Error: ${e}</li>`;
}
}
function closeCastOverlay() {
castOverlay.classList.add('hidden');
}
async function selectCastDevice(deviceName) {
closeCastOverlay();
// If checking same device, do nothing
if (deviceName === currentCastDevice) return;
// If switching mode, stop current playback
if (isPlaying) {
await stop();
}
if (deviceName) {
currentMode = 'cast';
currentCastDevice = deviceName;
castBtn.style.color = 'var(--success)';
} else {
currentMode = 'local';
currentCastDevice = null;
castBtn.style.color = 'var(--text-main)';
}
updateUI();
// Auto-play if we were playing? Let's stay stopped to be safe/explicit
// Or auto-play for better UX?
// Let's prompt user to play.
}
window.addEventListener('DOMContentLoaded', init);