Visually fix
This commit is contained in:
325
src/main.js
325
src/main.js
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user