Visually fix

This commit is contained in:
2026-01-02 13:25:10 +01:00
parent c5dc6b9dd4
commit e36bb1ab55
7 changed files with 632 additions and 73 deletions

View File

@@ -27,6 +27,19 @@ 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');
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
// Editor elements
const editBtn = document.getElementById('edit-stations-btn');
const editorOverlay = document.getElementById('editor-overlay');
const editorCloseBtn = document.getElementById('editor-close-btn');
const editorListEl = document.getElementById('editor-list');
const addStationForm = document.getElementById('add-station-form');
const usTitle = document.getElementById('us_title');
const usUrl = document.getElementById('us_url');
const usLogo = document.getElementById('us_logo');
const usWww = document.getElementById('us_www');
const usId = document.getElementById('us_id');
const usIndex = document.getElementById('us_index');
// Init
async function init() {
@@ -62,9 +75,39 @@ async function loadStations() {
// Filter out disabled stations and those without a stream URL
.filter((s) => s.enabled !== false && s.url && s.url.length > 0);
// Load user-defined stations from localStorage and merge
const user = loadUserStations();
const userNormalized = user
.map((s) => {
const name = s.title || s.name || s.id || 'UserStation';
const url = s.url || s.liveAudio || s.liveVideo || '';
return {
id: s.id || `user-${name.replace(/\s+/g, '-')}`,
name,
url,
logo: s.logo || '',
enabled: typeof s.enabled === 'boolean' ? s.enabled : true,
raw: s,
_user: true,
};
})
.filter((s) => s.url && s.url.length > 0);
// Append user stations after file stations
stations = stations.concat(userNormalized);
if (stations.length > 0) {
currentIndex = 0;
loadStation(currentIndex);
// Try to restore last selected station by id
const lastId = getLastStationId();
if (lastId) {
const found = stations.findIndex(s => s.id === lastId);
if (found >= 0) currentIndex = found;
else currentIndex = 0;
} else {
currentIndex = 0;
}
loadStation(currentIndex);
}
} catch (e) {
console.error('Failed to load stations', e);
@@ -72,6 +115,159 @@ async function loadStations() {
}
}
// --- User Stations (localStorage) ---
function loadUserStations() {
try {
const raw = localStorage.getItem('userStations');
if (!raw) return [];
return JSON.parse(raw);
} catch (e) {
console.error('Error reading user stations', e);
return [];
}
}
function saveUserStations(arr) {
try {
localStorage.setItem('userStations', JSON.stringify(arr || []));
} catch (e) {
console.error('Error saving user stations', e);
}
}
function openEditorOverlay() {
renderUserStationsList();
editorOverlay.classList.remove('hidden');
editorOverlay.setAttribute('aria-hidden', 'false');
}
function closeEditorOverlay() {
editorOverlay.classList.add('hidden');
editorOverlay.setAttribute('aria-hidden', 'true');
// clear form
addStationForm.reset();
usIndex.value = '';
}
function renderUserStationsList() {
const list = loadUserStations();
editorListEl.innerHTML = '';
if (!list || list.length === 0) {
editorListEl.innerHTML = '<li class="device"><div class="device-main">No user stations</div><div class="device-sub">Add your stream using the form below</div></li>';
return;
}
list.forEach((s, idx) => {
const li = document.createElement('li');
li.className = 'device';
const main = s.title || s.name || s.id || 'User Station';
const sub = s.url || '';
li.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div class=\"device-main\">${main}</div>
<div class=\"device-sub\">${sub}</div>
</div>
<div style=\"display:flex;gap:8px;align-items:center;\">
<button data-idx=\"${idx}\" class=\"btn edit-btn\" style=\"background:#6bd3ff;color:#042\">Edit</button>
<button data-idx=\"${idx}\" class=\"btn delete-btn\" style=\"background:#ff6b6b\">Delete</button>
</div>
</div>`;
editorListEl.appendChild(li);
});
// Attach handlers
editorListEl.querySelectorAll('.edit-btn').forEach(b => {
b.addEventListener('click', () => {
const idx = Number(b.getAttribute('data-idx'));
editUserStation(idx);
});
});
editorListEl.querySelectorAll('.delete-btn').forEach(b => {
b.addEventListener('click', () => {
const idx = Number(b.getAttribute('data-idx'));
deleteUserStation(idx);
});
});
}
function editUserStation(idx) {
const list = loadUserStations();
const s = list[idx];
if (!s) return;
usTitle.value = s.title || s.name || '';
usUrl.value = s.url || s.liveAudio || '';
usLogo.value = s.logo || '';
usWww.value = s.www || s.website || '';
usId.value = s.id || '';
usIndex.value = String(idx);
}
function deleteUserStation(idx) {
const list = loadUserStations();
list.splice(idx, 1);
saveUserStations(list);
// refresh stations in runtime
refreshStationsFromSources();
renderUserStationsList();
}
function refreshStationsFromSources() {
// reload stations.json and user stations into `stations` array
// For simplicity, re-run loadStations()
loadStations();
}
// Persist last-selected station id between sessions
function saveLastStationId(id) {
try {
if (!id) return;
localStorage.setItem('lastStationId', id);
} catch (e) {
console.error('Failed to save last station id', e);
}
}
function getLastStationId() {
try {
return localStorage.getItem('lastStationId');
} catch (e) {
return null;
}
}
// Handle form submit (add/update)
addStationForm && addStationForm.addEventListener('submit', (e) => {
e.preventDefault();
const list = loadUserStations();
const station = {
id: usId.value || `user-${Date.now()}`,
title: usTitle.value.trim(),
url: usUrl.value.trim(),
logo: usLogo.value.trim(),
www: usWww.value.trim(),
enabled: true,
};
const idx = usIndex.value === '' ? -1 : Number(usIndex.value);
if (idx >= 0 && idx < list.length) {
list[idx] = station;
} else {
list.push(station);
}
saveUserStations(list);
renderUserStationsList();
refreshStationsFromSources();
addStationForm.reset();
usIndex.value = '';
});
// Editor button handlers
editBtn && editBtn.addEventListener('click', openEditorOverlay);
editorCloseBtn && editorCloseBtn.addEventListener('click', closeEditorOverlay);
function setupEventListeners() {
playBtn.addEventListener('click', togglePlay);
prevBtn.addEventListener('click', playPrev);
@@ -94,10 +290,10 @@ function setupEventListeners() {
});
// Menu button - explicit functionality or placeholder?
// For now just log or maybe show about
document.getElementById('menu-btn').addEventListener('click', () => {
openStationsOverlay();
});
// Menu removed — header click opens stations via artwork placeholder
// Click artwork to open stations chooser
artworkPlaceholder && artworkPlaceholder.addEventListener('click', openStationsOverlay);
// Hotkeys?
}
@@ -113,9 +309,18 @@ function loadStation(index) {
// 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');
// Verify the logo exists before showing it
checkImageExists(station.logo).then((exists) => {
if (exists) {
logoImgEl.src = station.logo;
logoImgEl.classList.remove('hidden');
logoTextEl.classList.add('hidden');
} else {
logoImgEl.src = '';
logoImgEl.classList.add('hidden');
logoTextEl.classList.remove('hidden');
}
});
} else {
// Fallback to single-letter/logo text
logoImgEl.src = '';
@@ -130,6 +335,39 @@ function loadStation(index) {
}
}
// Check if an image URL is reachable and valid
function checkImageExists(url, timeout = 6000) {
return new Promise((resolve) => {
if (!url) return resolve(false);
try {
const img = new Image();
let timedOut = false;
const t = setTimeout(() => {
timedOut = true;
img.src = ''; // stop load
resolve(false);
}, timeout);
img.onload = () => {
if (!timedOut) {
clearTimeout(t);
resolve(true);
}
};
img.onerror = () => {
if (!timedOut) {
clearTimeout(t);
resolve(false);
}
};
// Bypass caching oddities by assigning after handlers
img.src = url;
} catch (e) {
resolve(false);
}
});
}
async function togglePlay() {
if (isPlaying) {
await stop();
@@ -202,6 +440,9 @@ async function playNext() {
currentIndex = (currentIndex + 1) % stations.length;
loadStation(currentIndex);
// persist selection
saveLastStationId(stations[currentIndex].id);
if (wasPlaying) await play();
}
@@ -215,6 +456,9 @@ async function playPrev() {
currentIndex = (currentIndex - 1 + stations.length) % stations.length;
loadStation(currentIndex);
// persist selection
saveLastStationId(stations[currentIndex].id);
if (wasPlaying) await play();
}
@@ -253,6 +497,8 @@ function handleVolumeInput() {
async function openCastOverlay() {
castOverlay.classList.remove('hidden');
castOverlay.setAttribute('aria-hidden', 'false');
// ensure cast overlay shows linear list style
deviceListEl.classList.remove('stations-grid');
deviceListEl.innerHTML = '<li class="device"><div class="device-main">Scanning...</div><div class="device-sub">Searching for speakers</div></li>';
try {
@@ -316,40 +562,75 @@ async function selectCastDevice(deviceName) {
window.addEventListener('DOMContentLoaded', init);
// Open overlay and show list of stations (used by menu/hamburger)
function openStationsOverlay() {
async function openStationsOverlay() {
castOverlay.classList.remove('hidden');
castOverlay.setAttribute('aria-hidden', 'false');
deviceListEl.innerHTML = '<li class="device"><div class="device-main">Loading...</div><div class="device-sub">Preparing stations</div></li>';
// If stations not loaded yet, show message
if (!stations || stations.length === 0) {
deviceListEl.classList.remove('stations-grid');
deviceListEl.innerHTML = '<li class="device"><div class="device-main">No stations found</div><div class="device-sub">Check your stations.json</div></li>';
return;
}
// Render stations as responsive grid of cards (2-3 per row depending on width)
deviceListEl.classList.add('stations-grid');
deviceListEl.innerHTML = '';
stations.forEach((s, idx) => {
for (let idx = 0; idx < stations.length; idx++) {
const s = stations[idx];
const li = document.createElement('li');
li.className = 'device' + (currentIndex === idx ? ' selected' : '');
li.className = 'station-card' + (currentIndex === idx ? ' selected' : '');
const logoUrl = s.logo || (s.raw && (s.raw.logo || s.raw.poster)) || '';
const title = s.name || s.title || s.id || 'Station';
const subtitle = (s.raw && s.raw.www) ? s.raw.www : (s.id || '');
li.innerHTML = `<div class="device-main">${s.name}</div><div class="device-sub">${subtitle}</div>`;
const left = document.createElement('div');
left.className = 'station-card-left';
// Check if logo exists, otherwise show fallback
const hasLogo = await checkImageExists(logoUrl);
if (hasLogo) {
const img = document.createElement('img');
img.className = 'station-card-logo';
img.src = logoUrl;
img.alt = `${title} logo`;
left.appendChild(img);
} else {
const fb = document.createElement('div');
fb.className = 'station-card-fallback';
fb.textContent = title.charAt(0).toUpperCase();
left.appendChild(fb);
}
const body = document.createElement('div');
body.className = 'station-card-body';
const tEl = document.createElement('div');
tEl.className = 'station-card-title';
tEl.textContent = title;
const sEl = document.createElement('div');
sEl.className = 'station-card-sub';
sEl.textContent = subtitle;
body.appendChild(tEl);
body.appendChild(sEl);
li.appendChild(left);
li.appendChild(body);
li.onclick = async () => {
// Always switch to local playback when selecting from stations menu
currentMode = 'local';
currentCastDevice = null;
castBtn.style.color = 'var(--text-main)';
// Select and play
currentIndex = idx;
// Remember this selection
saveLastStationId(stations[idx].id);
loadStation(currentIndex);
closeCastOverlay();
try {
await play();
} catch (e) {
console.error('Failed to play station from menu', e);
}
try { await play(); } catch (e) { console.error('Failed to play station from grid', e); }
};
deviceListEl.appendChild(li);
});
}
}