fix
This commit is contained in:
242
src/main.js
242
src/main.js
@@ -28,8 +28,9 @@ 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');
|
||||
const coverflowStageEl = document.getElementById('artwork-coverflow-stage');
|
||||
const coverflowPrevBtn = document.getElementById('artwork-prev');
|
||||
const coverflowNextBtn = document.getElementById('artwork-next');
|
||||
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
|
||||
// Global error handlers to avoid silent white screen and show errors in UI
|
||||
window.addEventListener('error', (ev) => {
|
||||
@@ -170,6 +171,7 @@ async function loadStations() {
|
||||
|
||||
console.debug('loadStations: loading station index', currentIndex);
|
||||
loadStation(currentIndex);
|
||||
renderCoverflow();
|
||||
// start polling for currentSong endpoints (if any)
|
||||
startCurrentSongPollers();
|
||||
}
|
||||
@@ -178,6 +180,179 @@ async function loadStations() {
|
||||
statusTextEl.textContent = 'Error loading stations';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Coverflow UI (3D-ish station cards like your reference image) ---
|
||||
let coverflowPointerId = null;
|
||||
let coverflowStartX = 0;
|
||||
let coverflowLastX = 0;
|
||||
let coverflowAccum = 0;
|
||||
let coverflowMoved = false;
|
||||
let coverflowWheelLock = false;
|
||||
|
||||
function renderCoverflow() {
|
||||
try {
|
||||
if (!coverflowStageEl) return;
|
||||
coverflowStageEl.innerHTML = '';
|
||||
|
||||
stations.forEach((s, idx) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'coverflow-item';
|
||||
item.dataset.idx = String(idx);
|
||||
|
||||
const logoUrl = s.logo || (s.raw && (s.raw.logo || s.raw.poster)) || '';
|
||||
if (logoUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.alt = `${s.name} logo`;
|
||||
img.src = logoUrl;
|
||||
img.addEventListener('error', () => {
|
||||
item.innerHTML = '';
|
||||
item.classList.add('fallback');
|
||||
item.textContent = s.name ? s.name.charAt(0).toUpperCase() : '?';
|
||||
});
|
||||
item.appendChild(img);
|
||||
} else {
|
||||
item.classList.add('fallback');
|
||||
item.textContent = s.name ? s.name.charAt(0).toUpperCase() : '?';
|
||||
}
|
||||
|
||||
// Click a card: if it's not selected, select it.
|
||||
// Double-click the selected card to open the full station grid.
|
||||
item.addEventListener('click', async (ev) => {
|
||||
if (coverflowMoved) return;
|
||||
const idxClicked = Number(item.dataset.idx);
|
||||
if (idxClicked !== currentIndex) {
|
||||
await setStationByIndex(idxClicked);
|
||||
}
|
||||
});
|
||||
item.addEventListener('dblclick', (ev) => {
|
||||
const idxClicked = Number(item.dataset.idx);
|
||||
if (idxClicked === currentIndex) openStationsOverlay();
|
||||
});
|
||||
|
||||
coverflowStageEl.appendChild(item);
|
||||
});
|
||||
|
||||
wireCoverflowInteractions();
|
||||
updateCoverflowTransforms();
|
||||
} catch (e) {
|
||||
console.debug('renderCoverflow failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function wireCoverflowInteractions() {
|
||||
try {
|
||||
const host = document.getElementById('artwork-coverflow');
|
||||
if (!host) return;
|
||||
|
||||
// Buttons
|
||||
if (coverflowPrevBtn) coverflowPrevBtn.onclick = () => setStationByIndex((currentIndex - 1 + stations.length) % stations.length);
|
||||
if (coverflowNextBtn) coverflowNextBtn.onclick = () => setStationByIndex((currentIndex + 1) % stations.length);
|
||||
|
||||
// Pointer drag (mouse/touch)
|
||||
host.onpointerdown = (ev) => {
|
||||
if (!stations || stations.length <= 1) return;
|
||||
coverflowPointerId = ev.pointerId;
|
||||
coverflowStartX = ev.clientX;
|
||||
coverflowLastX = ev.clientX;
|
||||
coverflowAccum = 0;
|
||||
coverflowMoved = false;
|
||||
try { host.setPointerCapture(ev.pointerId); } catch (e) {}
|
||||
};
|
||||
host.onpointermove = (ev) => {
|
||||
if (coverflowPointerId === null || ev.pointerId !== coverflowPointerId) return;
|
||||
const dx = ev.clientX - coverflowLastX;
|
||||
coverflowLastX = ev.clientX;
|
||||
if (Math.abs(ev.clientX - coverflowStartX) > 6) coverflowMoved = true;
|
||||
|
||||
// Accumulate movement; change station when threshold passed.
|
||||
coverflowAccum += dx;
|
||||
const threshold = 36;
|
||||
if (coverflowAccum >= threshold) {
|
||||
coverflowAccum = 0;
|
||||
setStationByIndex((currentIndex - 1 + stations.length) % stations.length);
|
||||
} else if (coverflowAccum <= -threshold) {
|
||||
coverflowAccum = 0;
|
||||
setStationByIndex((currentIndex + 1) % stations.length);
|
||||
}
|
||||
};
|
||||
host.onpointerup = (ev) => {
|
||||
if (coverflowPointerId === null || ev.pointerId !== coverflowPointerId) return;
|
||||
coverflowPointerId = null;
|
||||
// reset moved flag after click would have fired
|
||||
setTimeout(() => { coverflowMoved = false; }, 0);
|
||||
try { host.releasePointerCapture(ev.pointerId); } catch (e) {}
|
||||
};
|
||||
host.onpointercancel = (ev) => {
|
||||
coverflowPointerId = null;
|
||||
coverflowMoved = false;
|
||||
};
|
||||
|
||||
// Wheel: next/prev with debounce
|
||||
host.onwheel = (ev) => {
|
||||
if (!stations || stations.length <= 1) return;
|
||||
if (coverflowWheelLock) return;
|
||||
const delta = Math.abs(ev.deltaX) > Math.abs(ev.deltaY) ? ev.deltaX : ev.deltaY;
|
||||
if (Math.abs(delta) < 6) return;
|
||||
ev.preventDefault();
|
||||
coverflowWheelLock = true;
|
||||
if (delta > 0) setStationByIndex((currentIndex + 1) % stations.length);
|
||||
else setStationByIndex((currentIndex - 1 + stations.length) % stations.length);
|
||||
setTimeout(() => { coverflowWheelLock = false; }, 160);
|
||||
};
|
||||
} catch (e) {
|
||||
console.debug('wireCoverflowInteractions failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCoverflowTransforms() {
|
||||
try {
|
||||
if (!coverflowStageEl) return;
|
||||
const items = coverflowStageEl.querySelectorAll('.coverflow-item');
|
||||
const maxVisible = 3;
|
||||
items.forEach((el) => {
|
||||
const idx = Number(el.dataset.idx);
|
||||
const offset = idx - currentIndex;
|
||||
|
||||
if (Math.abs(offset) > maxVisible) {
|
||||
el.style.opacity = '0';
|
||||
el.style.pointerEvents = 'none';
|
||||
el.style.transform = 'translate(-50%, -50%) scale(0.6)';
|
||||
return;
|
||||
}
|
||||
|
||||
const abs = Math.abs(offset);
|
||||
const dir = offset === 0 ? 0 : (offset > 0 ? 1 : -1);
|
||||
|
||||
const translateX = dir * (abs * 78);
|
||||
const translateZ = -abs * 70;
|
||||
const rotateY = dir * (-28 * abs);
|
||||
const scale = 1 - abs * 0.12;
|
||||
const opacity = 1 - abs * 0.18;
|
||||
const zIndex = 100 - abs;
|
||||
|
||||
el.style.opacity = String(opacity);
|
||||
el.style.zIndex = String(zIndex);
|
||||
el.style.pointerEvents = 'auto';
|
||||
el.style.transform = `translate(-50%, -50%) translateX(${translateX}px) translateZ(${translateZ}px) rotateY(${rotateY}deg) scale(${scale})`;
|
||||
|
||||
if (offset === 0) el.classList.add('selected');
|
||||
else el.classList.remove('selected');
|
||||
});
|
||||
} catch (e) {
|
||||
console.debug('updateCoverflowTransforms failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setStationByIndex(idx) {
|
||||
if (idx < 0 || idx >= stations.length) return;
|
||||
const wasPlaying = isPlaying;
|
||||
if (wasPlaying) await stop();
|
||||
currentIndex = idx;
|
||||
saveLastStationId(stations[currentIndex].id);
|
||||
loadStation(currentIndex);
|
||||
updateCoverflowTransforms();
|
||||
if (wasPlaying) await play();
|
||||
}
|
||||
|
||||
|
||||
// --- Current Song Polling ---
|
||||
@@ -628,8 +803,6 @@ function ensureArtworkPointerFallback() {
|
||||
|
||||
// Quick inline style fallback (helps when CSS is overridden)
|
||||
try { ap.style.cursor = 'pointer'; } catch (e) {}
|
||||
try { if (logoImgEl) logoImgEl.style.cursor = 'pointer'; } catch (e) {}
|
||||
try { if (logoTextEl) logoTextEl.style.cursor = 'pointer'; } catch (e) {}
|
||||
|
||||
let active = false;
|
||||
const onMove = (ev) => {
|
||||
@@ -668,34 +841,8 @@ function loadStation(index) {
|
||||
if (nowArtistEl) nowArtistEl.textContent = '';
|
||||
if (nowTitleEl) nowTitleEl.textContent = '';
|
||||
|
||||
// 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) {
|
||||
// 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 = '';
|
||||
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');
|
||||
}
|
||||
// Sync coverflow transforms (if present)
|
||||
try { updateCoverflowTransforms(); } catch (e) {}
|
||||
// When loading a station, ensure only this station's poller runs
|
||||
try { startCurrentSongPollers(); } catch (e) { console.debug('startCurrentSongPollers failed in loadStation', e); }
|
||||
}
|
||||
@@ -798,33 +945,15 @@ async function playNext() {
|
||||
|
||||
// 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);
|
||||
|
||||
// persist selection
|
||||
saveLastStationId(stations[currentIndex].id);
|
||||
|
||||
if (wasPlaying) await play();
|
||||
const nextIndex = (currentIndex + 1) % stations.length;
|
||||
await setStationByIndex(nextIndex);
|
||||
}
|
||||
|
||||
async function playPrev() {
|
||||
if (stations.length === 0) return;
|
||||
|
||||
const wasPlaying = isPlaying;
|
||||
|
||||
if (wasPlaying) await stop();
|
||||
|
||||
currentIndex = (currentIndex - 1 + stations.length) % stations.length;
|
||||
loadStation(currentIndex);
|
||||
|
||||
// persist selection
|
||||
saveLastStationId(stations[currentIndex].id);
|
||||
|
||||
if (wasPlaying) await play();
|
||||
const prevIndex = (currentIndex - 1 + stations.length) % stations.length;
|
||||
await setStationByIndex(prevIndex);
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
@@ -999,10 +1128,7 @@ async function openStationsOverlay() {
|
||||
currentMode = 'local';
|
||||
currentCastDevice = null;
|
||||
castBtn.style.color = 'var(--text-main)';
|
||||
currentIndex = idx;
|
||||
// Remember this selection
|
||||
saveLastStationId(stations[idx].id);
|
||||
loadStation(currentIndex);
|
||||
await setStationByIndex(idx);
|
||||
closeCastOverlay();
|
||||
try { await play(); } catch (e) { console.error('Failed to play station from grid', e); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user