This commit is contained in:
2026-01-11 08:19:27 +01:00
parent f2732b36f2
commit bdd3e30f14
17 changed files with 3088 additions and 23 deletions

View File

@@ -6,6 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RadioPlayer</title>
<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#1f1f2e">
<link rel="apple-touch-icon" href="assets/favicon_io/apple-touch-icon.png">
<script src="main.js" defer type="module"></script>
</head>

View File

@@ -106,7 +106,7 @@ async function loadStations() {
try {
// stop any existing pollers before reloading stations
stopCurrentSongPollers();
const resp = await fetch('stations.json');
const resp = await fetch('/stations.json');
const raw = await resp.json();
// Normalize station objects so the rest of the app can rely on `name` and `url`.
@@ -152,6 +152,11 @@ async function loadStations() {
// Append user stations after file stations
stations = stations.concat(userNormalized);
// Debug: report how many stations we have after loading
try {
console.debug('loadStations: loaded stations count:', stations.length);
} catch (e) {}
if (stations.length > 0) {
// Try to restore last selected station by id
const lastId = getLastStationId();
@@ -163,6 +168,7 @@ async function loadStations() {
currentIndex = 0;
}
console.debug('loadStations: loading station index', currentIndex);
loadStation(currentIndex);
// start polling for currentSong endpoints (if any)
startCurrentSongPollers();
@@ -414,10 +420,9 @@ function updateNowPlayingUI() {
if (!station) return;
if (nowPlayingEl && nowArtistEl && nowTitleEl) {
// Show now-playing if we have either an artist or a title (some stations only provide title)
if (station.currentSongInfo && (station.currentSongInfo.artist || station.currentSongInfo.title)) {
nowArtistEl.textContent = station.currentSongInfo.artist || '';
nowTitleEl.textContent = station.currentSongInfo.title || '';
if (station.currentSongInfo && station.currentSongInfo.artist && station.currentSongInfo.title) {
nowArtistEl.textContent = station.currentSongInfo.artist;
nowTitleEl.textContent = station.currentSongInfo.title;
nowPlayingEl.classList.remove('hidden');
} else {
nowArtistEl.textContent = '';
@@ -680,13 +685,14 @@ function loadStation(index) {
}
});
} else {
// Fallback: show the full station name when no logo is provided
// Fallback to single-letter/logo text
logoImgEl.src = '';
logoImgEl.classList.add('hidden');
try {
logoTextEl.textContent = (station.name || '').trim();
} catch (e) {
logoTextEl.textContent = '';
const numberMatch = station.name.match(/\d+/);
if (numberMatch) {
logoTextEl.textContent = numberMatch[0];
} else {
logoTextEl.textContent = station.name.charAt(0).toUpperCase();
}
logoTextEl.classList.remove('hidden');
}
@@ -922,6 +928,15 @@ async function selectCastDevice(deviceName) {
window.addEventListener('DOMContentLoaded', init);
// Register Service Worker for PWA installation (non-disruptive)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js')
.then((reg) => console.log('ServiceWorker registered:', reg.scope))
.catch((err) => console.debug('ServiceWorker registration failed:', err));
});
}
// Open overlay and show list of stations (used by menu/hamburger)
async function openStationsOverlay() {
castOverlay.classList.remove('hidden');

22
src/manifest.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "RadioPlayer",
"short_name": "Radio",
"description": "RadioPlayer — stream radio stations from the web",
"start_url": ".",
"scope": ".",
"display": "standalone",
"background_color": "#1f1f2e",
"theme_color": "#1f1f2e",
"icons": [
{
"src": "assets/favicon_io/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/favicon_io/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

48
src/sw.js Normal file
View File

@@ -0,0 +1,48 @@
const CACHE_NAME = 'radiocast-core-v1';
const CORE_ASSETS = [
'.',
'index.html',
'main.js',
'styles.css',
'stations.json',
'assets/favicon_io/android-chrome-192x192.png',
'assets/favicon_io/android-chrome-512x512.png',
'assets/favicon_io/apple-touch-icon.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.map((k) => { if (k !== CACHE_NAME) return caches.delete(k); return null; })
))
);
});
self.addEventListener('fetch', (event) => {
// Only handle GET requests
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((networkResp) => {
// Optionally cache new resources (best-effort)
try {
const respClone = networkResp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, respClone)).catch(()=>{});
} catch (e) {}
return networkResp;
}).catch(() => {
// If offline and HTML navigation, return cached index.html
if (event.request.mode === 'navigate') return caches.match('index.html');
return new Response('', { status: 503, statusText: 'Service Unavailable' });
});
})
);
});