Refine mobile layout and PWA paths

This commit is contained in:
2026-04-26 17:11:39 +02:00
parent 0864a28593
commit 41a5472582
5 changed files with 384 additions and 70 deletions
+130 -5
View File
@@ -34,10 +34,14 @@ let stationCatalogState = 'idle';
let stationCatalogError = '';
let playbackError = '';
let stationCountryFilterOpen = false;
let artworkShineTimeoutId = null;
let artworkShineClearTimeoutId = null;
let stationTitleRafId = null;
const STATION_LIBRARY_PAGE_SIZE = 12;
const ARTWORK_SHINE_EFFECTS = ['shine-sweep', 'shine-glint', 'shine-flare'];
const RADIO_PLACEHOLDER_LOGO = '/images/radio-placeholder.svg';
const RADIO_PLACEHOLDER_LOGO = `${import.meta.env.BASE_URL}images/radio-placeholder.svg`;
// UI Elements
const stationNameEl = document.getElementById('station-name');
@@ -112,6 +116,54 @@ const castOutputText = document.getElementById('cast-output-text');
// ── Utilities ────────────────────────────────────────────────────────────────
function prefersReducedMotion() {
try {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
} catch (e) {
return false;
}
}
function clearArtworkShineTimers() {
if (artworkShineTimeoutId) {
clearTimeout(artworkShineTimeoutId);
artworkShineTimeoutId = null;
}
if (artworkShineClearTimeoutId) {
clearTimeout(artworkShineClearTimeoutId);
artworkShineClearTimeoutId = null;
}
if (artworkPlaceholder) {
artworkPlaceholder.classList.remove(...ARTWORK_SHINE_EFFECTS);
}
}
function scheduleArtworkShine() {
if (!artworkPlaceholder || prefersReducedMotion()) return;
const trigger = () => {
artworkShineTimeoutId = null;
clearArtworkShineTimers();
if (!artworkPlaceholder || prefersReducedMotion()) return;
const effect = ARTWORK_SHINE_EFFECTS[Math.floor(Math.random() * ARTWORK_SHINE_EFFECTS.length)];
const effectDuration = 1800 + Math.floor(Math.random() * 900);
artworkPlaceholder.classList.add(effect);
artworkShineClearTimeoutId = window.setTimeout(() => {
artworkPlaceholder.classList.remove(effect);
artworkShineClearTimeoutId = null;
}, effectDuration);
const nextDelay = 7000 + Math.floor(Math.random() * 15000);
artworkShineTimeoutId = window.setTimeout(trigger, nextDelay);
};
clearArtworkShineTimers();
trigger();
}
const STATION_THEMES = [
{ accent: '#4dd7c8', accent2: '#ffb45c', accent3: '#8fb3ff', page: '#171b22', panel: '#111821' },
{ accent: '#ff7aa8', accent2: '#ffd166', accent3: '#8fb3ff', page: '#22151d', panel: '#1b131a' },
@@ -255,7 +307,18 @@ function getMetadataFetchUrl(url) {
if (!url || typeof url !== 'string') return '';
const safeUrl = toHttpsIfHttp(url) || url;
if (!import.meta.env.DEV) return safeUrl;
if (!import.meta.env.DEV) {
try {
const parsed = new URL(safeUrl);
if (parsed.hostname === 'data.radio.si') {
return '';
}
} catch (e) {
return safeUrl;
}
return safeUrl;
}
try {
const parsed = new URL(safeUrl);
@@ -538,6 +601,64 @@ function getStationTitle(station) {
return station?.name || station?.title || station?.id || 'Station';
}
function isMobileTitleViewport() {
return window.matchMedia('(max-width: 760px)').matches;
}
function renderStationTitle(title, shouldScroll = false) {
if (!stationNameEl) return;
stationNameEl.replaceChildren();
if (!shouldScroll) {
stationNameEl.classList.remove('station-title-marquee');
const plainTitle = document.createElement('span');
plainTitle.className = 'station-title-text';
plainTitle.textContent = title;
stationNameEl.appendChild(plainTitle);
return;
}
stationNameEl.classList.add('station-title-marquee');
const track = document.createElement('span');
track.className = 'station-title-track';
const firstCopy = document.createElement('span');
firstCopy.className = 'station-title-copy';
firstCopy.textContent = title;
const secondCopy = document.createElement('span');
secondCopy.className = 'station-title-copy';
secondCopy.setAttribute('aria-hidden', 'true');
secondCopy.textContent = title;
track.append(firstCopy, secondCopy);
stationNameEl.appendChild(track);
}
function updateStationTitleLayout(title) {
if (!stationNameEl) return;
const titleText = String(title || '').trim() || 'Station';
if (stationTitleRafId) {
cancelAnimationFrame(stationTitleRafId);
stationTitleRafId = null;
}
renderStationTitle(titleText, false);
if (!isMobileTitleViewport()) return;
stationTitleRafId = window.requestAnimationFrame(() => {
stationTitleRafId = null;
if (!stationNameEl || !isMobileTitleViewport()) return;
if (stationNameEl.scrollWidth > stationNameEl.clientWidth + 2) {
renderStationTitle(titleText, true);
}
});
}
function getStationCountry(station) {
return station?.country || station?.countryCode || station?.region || '';
}
@@ -1560,7 +1681,7 @@ function loadStation(index) {
applyStationTheme(station);
if (stationNameEl) stationNameEl.textContent = station.name;
updateStationTitleLayout(station.name);
if (stationSubtitleEl) stationSubtitleEl.textContent = getStationDetails(station) || 'Live stream';
if (nowPlayingEl) nowPlayingEl.classList.add('hidden');
if (nowArtistEl) nowArtistEl.textContent = '';
@@ -2131,7 +2252,10 @@ function setupEventListeners() {
artworkPlaceholder?.addEventListener('click', openStationLibrary);
castOutputBtn?.addEventListener('click', toggleCastBothMode);
window.addEventListener('resize', updateCoverflowTransforms);
window.addEventListener('resize', () => {
updateCoverflowTransforms();
updateStationTitleLayout(getStationTitle(stations[currentIndex]));
});
document.addEventListener('click', (ev) => {
if (!stationCountryFilterOpen) return;
if (stationCountryFilterWrapEl?.contains(ev.target)) return;
@@ -2202,6 +2326,7 @@ async function init() {
await loadStations();
setupEventListeners();
ensureArtworkPointerFallback();
scheduleArtworkShine();
updateUI();
// Update Media Session when station or song changes
@@ -2228,7 +2353,7 @@ if ('serviceWorker' in navigator && import.meta.env.DEV) {
});
} else if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
navigator.serviceWorker.register(`${import.meta.env.BASE_URL}sw.js`)
.then((reg) => console.log('ServiceWorker registered:', reg.scope))
.catch((err) => console.debug('ServiceWorker registration failed:', err));
});