Refine mobile layout and PWA paths
This commit is contained in:
+130
-5
@@ -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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user