fix
This commit is contained in:
193
src/main.js
193
src/main.js
@@ -1,6 +1,10 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { getCurrentWindow } = window.__TAURI__.window;
|
||||
|
||||
// In Tauri, the WebView may block insecure (http) images as mixed-content.
|
||||
// We can optionally fetch such images via backend and render as data: URLs.
|
||||
const runningInTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||||
|
||||
// State
|
||||
let stations = [];
|
||||
let currentIndex = 0;
|
||||
@@ -38,6 +42,93 @@ const coverflowNextBtn = document.getElementById('artwork-next');
|
||||
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
|
||||
const logoTextEl = document.querySelector('.station-logo-text');
|
||||
const logoImgEl = document.getElementById('station-logo-img');
|
||||
|
||||
function toHttpsIfHttp(url) {
|
||||
if (!url || typeof url !== 'string') return '';
|
||||
return url.startsWith('http://') ? ('https://' + url.slice('http://'.length)) : url;
|
||||
}
|
||||
|
||||
function uniqueNonEmpty(urls) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const u of urls) {
|
||||
if (!u || typeof u !== 'string') continue;
|
||||
const trimmed = u.trim();
|
||||
if (!trimmed) continue;
|
||||
if (seen.has(trimmed)) continue;
|
||||
seen.add(trimmed);
|
||||
out.push(trimmed);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function setImgWithFallback(imgEl, urls, onFinalError) {
|
||||
let dataFallbackUrls = [];
|
||||
// Backward compatible signature; allow passing { dataFallbackUrls } as 4th param.
|
||||
// (Implemented below via arguments inspection.)
|
||||
|
||||
if (arguments.length >= 4 && arguments[3] && typeof arguments[3] === 'object') {
|
||||
const opt = arguments[3];
|
||||
if (Array.isArray(opt.dataFallbackUrls)) dataFallbackUrls = opt.dataFallbackUrls;
|
||||
}
|
||||
|
||||
const candidates = uniqueNonEmpty(urls);
|
||||
let i = 0;
|
||||
let dataIdx = 0;
|
||||
let triedData = false;
|
||||
|
||||
if (!imgEl || candidates.length === 0) {
|
||||
if (imgEl) {
|
||||
imgEl.onload = null;
|
||||
imgEl.onerror = null;
|
||||
imgEl.src = '';
|
||||
}
|
||||
if (onFinalError) onFinalError();
|
||||
return;
|
||||
}
|
||||
|
||||
const tryNext = () => {
|
||||
if (i >= candidates.length) {
|
||||
// If direct loads failed and we're in Tauri, try fetching via backend and set as data URL.
|
||||
if (runningInTauri && !triedData && dataFallbackUrls && dataFallbackUrls.length > 0) {
|
||||
triedData = true;
|
||||
const dataCandidates = uniqueNonEmpty(dataFallbackUrls);
|
||||
const tryData = () => {
|
||||
if (dataIdx >= dataCandidates.length) {
|
||||
if (onFinalError) onFinalError();
|
||||
return;
|
||||
}
|
||||
const u = dataCandidates[dataIdx++];
|
||||
invoke('fetch_image_data_url', { url: u })
|
||||
.then((dataUrl) => {
|
||||
// Once we have a data URL, we can stop the fallback chain.
|
||||
imgEl.src = dataUrl;
|
||||
})
|
||||
.catch(() => tryData());
|
||||
};
|
||||
tryData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (onFinalError) onFinalError();
|
||||
return;
|
||||
}
|
||||
const nextUrl = candidates[i++];
|
||||
imgEl.src = nextUrl;
|
||||
};
|
||||
|
||||
imgEl.onload = () => {
|
||||
// keep last successful src
|
||||
};
|
||||
imgEl.onerror = () => {
|
||||
tryNext();
|
||||
};
|
||||
|
||||
// Some CDNs block referrers; this can improve logo load reliability.
|
||||
try { imgEl.referrerPolicy = 'no-referrer'; } catch (e) {}
|
||||
|
||||
tryNext();
|
||||
}
|
||||
// Global error handlers to avoid silent white screen and show errors in UI
|
||||
window.addEventListener('error', (ev) => {
|
||||
try {
|
||||
@@ -286,22 +377,29 @@ function renderCoverflow() {
|
||||
item.dataset.idx = String(idx);
|
||||
|
||||
const rawLogoUrl = s.logo || (s.raw && (s.raw.logo || s.raw.poster)) || '';
|
||||
const logoUrl = rawLogoUrl && rawLogoUrl.startsWith('http://')
|
||||
? ('https://' + rawLogoUrl.slice('http://'.length))
|
||||
: rawLogoUrl;
|
||||
if (logoUrl) {
|
||||
const fallbackLabel = (s && s.name ? String(s.name) : '?').trim();
|
||||
item.title = fallbackLabel;
|
||||
|
||||
if (rawLogoUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.alt = `${s.name} logo`;
|
||||
img.src = logoUrl;
|
||||
img.addEventListener('error', () => {
|
||||
|
||||
// Try https first (avoids mixed-content blocks), then fall back to original.
|
||||
const candidates = [
|
||||
toHttpsIfHttp(rawLogoUrl),
|
||||
rawLogoUrl,
|
||||
];
|
||||
|
||||
setImgWithFallback(img, candidates, () => {
|
||||
item.innerHTML = '';
|
||||
item.classList.add('fallback');
|
||||
item.textContent = s.name ? s.name.charAt(0).toUpperCase() : '?';
|
||||
});
|
||||
item.textContent = fallbackLabel;
|
||||
}, { dataFallbackUrls: [rawLogoUrl] });
|
||||
|
||||
item.appendChild(img);
|
||||
} else {
|
||||
item.classList.add('fallback');
|
||||
item.textContent = s.name ? s.name.charAt(0).toUpperCase() : '?';
|
||||
item.textContent = fallbackLabel;
|
||||
}
|
||||
|
||||
// Click a card: if it's not selected, select it.
|
||||
@@ -334,12 +432,32 @@ function wireCoverflowInteractions() {
|
||||
if (!host) return;
|
||||
|
||||
// Buttons
|
||||
if (coverflowPrevBtn) coverflowPrevBtn.onclick = () => setStationByIndex((currentIndex - 1 + stations.length) % stations.length);
|
||||
if (coverflowNextBtn) coverflowNextBtn.onclick = () => setStationByIndex((currentIndex + 1) % stations.length);
|
||||
// IMPORTANT: prevent the coverflow drag handler (pointer capture) from swallowing button clicks.
|
||||
if (coverflowPrevBtn) {
|
||||
coverflowPrevBtn.onpointerdown = (ev) => { try { ev.stopPropagation(); } catch (e) {} };
|
||||
coverflowPrevBtn.onclick = (ev) => {
|
||||
try { ev.stopPropagation(); ev.preventDefault(); } catch (e) {}
|
||||
setStationByIndex((currentIndex - 1 + stations.length) % stations.length);
|
||||
};
|
||||
}
|
||||
if (coverflowNextBtn) {
|
||||
coverflowNextBtn.onpointerdown = (ev) => { try { ev.stopPropagation(); } catch (e) {} };
|
||||
coverflowNextBtn.onclick = (ev) => {
|
||||
try { ev.stopPropagation(); ev.preventDefault(); } catch (e) {}
|
||||
setStationByIndex((currentIndex + 1) % stations.length);
|
||||
};
|
||||
}
|
||||
|
||||
// Pointer drag (mouse/touch)
|
||||
host.onpointerdown = (ev) => {
|
||||
if (!stations || stations.length <= 1) return;
|
||||
|
||||
// If the user clicked the arrow buttons, let the button handler run.
|
||||
// Otherwise pointer capture can prevent the click from reaching the button.
|
||||
try {
|
||||
if (ev.target && ev.target.closest && ev.target.closest('.coverflow-arrow')) return;
|
||||
} catch (e) {}
|
||||
|
||||
coverflowPointerId = ev.pointerId;
|
||||
coverflowStartX = ev.clientX;
|
||||
coverflowLastX = ev.clientX;
|
||||
@@ -397,10 +515,17 @@ function updateCoverflowTransforms() {
|
||||
try {
|
||||
if (!coverflowStageEl) return;
|
||||
const items = coverflowStageEl.querySelectorAll('.coverflow-item');
|
||||
const n = stations ? stations.length : 0;
|
||||
if (n <= 0) return;
|
||||
const maxVisible = 3;
|
||||
items.forEach((el) => {
|
||||
const idx = Number(el.dataset.idx);
|
||||
const offset = idx - currentIndex;
|
||||
// Treat the station list as circular so the coverflow loops infinitely.
|
||||
// This makes the "previous" of index 0 be the last station, etc.
|
||||
let offset = idx - currentIndex;
|
||||
const half = Math.floor(n / 2);
|
||||
if (offset > half) offset -= n;
|
||||
if (offset < -half) offset += n;
|
||||
|
||||
if (Math.abs(offset) > maxVisible) {
|
||||
el.style.opacity = '0';
|
||||
@@ -933,36 +1058,35 @@ function loadStation(index) {
|
||||
// Update main artwork logo (best-effort). Many station logo URLs are http; try https first.
|
||||
try {
|
||||
if (logoTextEl && station && station.name) {
|
||||
const numberMatch = station.name.match(/\d+/);
|
||||
logoTextEl.textContent = numberMatch ? numberMatch[0] : station.name.charAt(0).toUpperCase();
|
||||
logoTextEl.textContent = String(station.name).trim();
|
||||
logoTextEl.classList.add('logo-name');
|
||||
}
|
||||
|
||||
const rawLogo = (station && (station.logo || (station.raw && (station.raw.logo || station.raw.poster)))) || '';
|
||||
const logoUrl = rawLogo && rawLogo.startsWith('http://') ? ('https://' + rawLogo.slice('http://'.length)) : rawLogo;
|
||||
const rawLogo = (station && (station.logo || (station.raw && (station.raw.logo || '')))) || '';
|
||||
const rawPoster = (station && ((station.raw && station.raw.poster) || station.poster || '')) || '';
|
||||
|
||||
if (logoImgEl) {
|
||||
logoImgEl.onload = null;
|
||||
logoImgEl.onerror = null;
|
||||
// Show fallback until load completes.
|
||||
logoImgEl.classList.add('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.remove('hidden');
|
||||
|
||||
if (logoUrl) {
|
||||
logoImgEl.onload = () => {
|
||||
logoImgEl.classList.remove('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.add('hidden');
|
||||
};
|
||||
logoImgEl.onerror = () => {
|
||||
logoImgEl.classList.add('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.remove('hidden');
|
||||
};
|
||||
const candidates = uniqueNonEmpty([
|
||||
toHttpsIfHttp(rawLogo),
|
||||
rawLogo,
|
||||
toHttpsIfHttp(rawPoster),
|
||||
rawPoster,
|
||||
]);
|
||||
|
||||
logoImgEl.src = logoUrl;
|
||||
// Show fallback until load completes.
|
||||
setImgWithFallback(logoImgEl, candidates, () => {
|
||||
logoImgEl.classList.add('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.remove('hidden');
|
||||
} else {
|
||||
logoImgEl.src = '';
|
||||
logoImgEl.classList.add('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.remove('hidden');
|
||||
}
|
||||
}, { dataFallbackUrls: [rawLogo, rawPoster] });
|
||||
|
||||
// If something loads successfully, show it.
|
||||
logoImgEl.onload = () => {
|
||||
logoImgEl.classList.remove('hidden');
|
||||
if (logoTextEl) logoTextEl.classList.add('hidden');
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// non-fatal
|
||||
@@ -1193,7 +1317,6 @@ window.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// Service worker is useful for the PWA, but it can cause confusing caching during
|
||||
// Tauri development because it may serve an older cached `index.html`.
|
||||
const runningInTauri = !!(window.__TAURI__ && window.__TAURI__.core);
|
||||
if ('serviceWorker' in navigator) {
|
||||
if (runningInTauri) {
|
||||
// Best-effort cleanup so the desktop app always reflects local file changes.
|
||||
|
||||
Reference in New Issue
Block a user