diff --git a/README.md b/README.md index a86d8c9..c4b3a6f 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,14 @@ To change the default window size, edit `src-tauri/tauri.conf.json`: [Add License Information Here] -## Release v0.1 +## Release v0.2 -Initial public preview (v0.1) — a minimal, working RadioPlayer experience: +Public beta (v0.2) — updates since v0.1: -- Custom CAF Receiver UI (HTML/CSS/JS) in `receiver/` with branded artwork and playback status. -- Plays LIVE stream: `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`). -- Desktop sidecar (`sidecar/index.js`) launches the Default Media Receiver and sends LOAD commands; launch flow now retries if the device reports `NOT_ALLOWED` by stopping existing sessions first. +- **Android build support:** Project includes Android build scripts and Gradle wrappers. See [scripts/build-android.sh](scripts/build-android.sh) and [build-android.ps1](build-android.ps1). Prebuilt native helper binaries are available in `src-tauri/binaries/` for convenience. +- **Web receiver & webapp:** The `receiver/` folder contains a Custom CAF Receiver UI (HTML/CSS/JS) and the `webapp/` folder provides a standalone web distribution for hosting the app in browsers or PWAs. +- **Sidecar improvements:** `sidecar/index.js` now retries launches when devices return `NOT_ALLOWED` by attempting to stop existing sessions before retrying. Check sidecar logs for `Launch NOT_ALLOWED` messages and retry attempts. +- **LIVE stream:** The app continues to support the LIVE stream `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`). Included receiver files: @@ -140,6 +141,6 @@ npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem Sidecar / troubleshoot -- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will now attempt to stop any existing sessions on the device and retry the launch (best-effort). Check sidecar logs for `Launch NOT_ALLOWED` and subsequent retry attempts. +- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will attempt to stop any existing sessions on the device and retry the launch (best-effort). Check sidecar logs for `Launch NOT_ALLOWED` and subsequent retry attempts. - Note: the sidecar uses `castv2-client` (not the official Google sender SDK). Group/stereo behavior may vary across device types — for full sender capabilities consider adding an official sender implementation. diff --git a/android/app/src/main/assets/styles.css b/android/app/src/main/assets/styles.css index c01becc..6ee55ec 100644 --- a/android/app/src/main/assets/styles.css +++ b/android/app/src/main/assets/styles.css @@ -18,13 +18,6 @@ cursor: default; } -/* Show pointer cursor for interactive / clickable elements (override global default) */ -a, a[href], button, input[type="button"], input[type="submit"], -[role="button"], [onclick], .clickable, .icon-btn, .control-btn, label[for], -.station-item, [tabindex]:not([tabindex="-1"]) { - cursor: pointer !important; -} - /* Hide Scrollbars */ ::-webkit-scrollbar { display: none; diff --git a/src/index.html b/src/index.html index 8a82012..9b67b04 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,9 @@ RadioPlayer + + + diff --git a/src/main.js b/src/main.js index f15ea03..a53d9a5 100644 --- a/src/main.js +++ b/src/main.js @@ -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'); diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..a27e292 --- /dev/null +++ b/src/manifest.json @@ -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" + } + ] +} diff --git a/src/sw.js b/src/sw.js new file mode 100644 index 0000000..fa201a5 --- /dev/null +++ b/src/sw.js @@ -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' }); + }); + }) + ); +}); diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 0000000..a025f30 --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,19 @@ +# RadioCast Webapp (Vite) + +This folder contains a minimal Vite scaffold that loads the existing app code +from the workspace `src` folder. It is intentionally lightweight and keeps the +original project files unchanged. + +Quick start: + +```powershell +cd webapp +npm install +npm run dev +# open http://localhost:5173 +``` + +Notes: +- The Vite config allows reading files from the parent workspace so the + existing `src/main.js` is reused. +- You can `npm run build` here to produce a static build in `webapp/dist`. diff --git a/webapp/assets/favicon_io/site.webmanifest b/webapp/assets/favicon_io/site.webmanifest new file mode 100644 index 0000000..1dd9112 --- /dev/null +++ b/webapp/assets/favicon_io/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 0000000..065e57d --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,218 @@ + + + + + + + RadioPlayer + + + + + + + + + +
+
+
+ +
+
+
+ + + + +
+ +
+
+ +
+
+ + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + 1 +
+
+
+ +
+

+ +

+ +
+ + +
+
+
+
+
+
+ +
+ + + + + +
+ +
+ +
+ +
+ 50% +
+ + + + + + + +
+
+ + + diff --git a/webapp/manifest.json b/webapp/manifest.json new file mode 100644 index 0000000..a27e292 --- /dev/null +++ b/webapp/manifest.json @@ -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" + } + ] +} diff --git a/webapp/package-lock.json b/webapp/package-lock.json new file mode 100644 index 0000000..f43a09e --- /dev/null +++ b/webapp/package-lock.json @@ -0,0 +1,942 @@ +{ + "name": "radiocast-webapp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "radiocast-webapp", + "version": "0.1.0", + "devDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/webapp/package.json b/webapp/package.json new file mode 100644 index 0000000..148ec13 --- /dev/null +++ b/webapp/package.json @@ -0,0 +1,14 @@ +{ + "name": "radiocast-webapp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --port 5174" + }, + "devDependencies": { + "vite": "^5.0.0" + } +} diff --git a/webapp/src/main.js b/webapp/src/main.js new file mode 100644 index 0000000..08d2863 --- /dev/null +++ b/webapp/src/main.js @@ -0,0 +1,20 @@ +// RadioCast webapp entry (web-only) +// Removed Tauri-specific shims so this file runs in a plain browser. + +document.addEventListener('DOMContentLoaded', () => { + const app = document.getElementById('app'); + if (!app) { + console.warn('No #app element found'); + return; + } + + app.innerHTML = ` +
+

RadioCast (Web)

+

Running as a plain web application (no Tauri).

+
Status: Idle
+
+ `; + + console.log('RadioCast webapp started (web mode)'); +}); diff --git a/webapp/stations.json b/webapp/stations.json new file mode 100644 index 0000000..05b58d3 --- /dev/null +++ b/webapp/stations.json @@ -0,0 +1,800 @@ +[ + { + "id": "Radio1", + "title": "Radio 1", + "slogan": "Več dobre glasbe", + "logo": "http://datacache.radio.si/api/radiostations/logo/radio1.svg", + "liveAudio": "http://live.radio1.si/Radio1", + "liveVideo": null, + "poster": "", + "lastSongs": "http://data.radio.si/api/lastsongsxml/radio1/json", + "epg": "http://spored.radio.si/api/now/radio1", + "defaultText": "www.radio1.si", + "www": "https://www.radio1.si", + "mountPoints": [ + "Radio1", + "Radio1BK", + "Radio1CE", + "Radio1GOR", + "Radio1KOR", + "Radio1LI", + "Radio1MB", + "Radio1NM", + "Radio1OB", + "Radio1PO", + "Radio1PR", + "Radio1PRI", + "Radio1PT", + "Radio1RIB", + "Radio1VE", + "Radio1VR", + "Radio1SAV" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38651300300" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "http://m.radio1.si" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "http://www.youtube.com/user/radio1slovenia" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "http://facebook.com/RadioEna" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "http://www.instagram.com/radio1slo" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radio1?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=50668", + "rpUid": "705167", + "dabUser": "radio1", + "dabPass": "sUbSGhmzdwKQT", + "dabDefaultImg": "http://media.radio.si/logo/dns/radio1/320x240.png", + "small": false + }, + { + "id": "Aktual", + "title": "Radio Aktual", + "slogan": "Narejen za vaša ušesa", + "logo": "http://datacache.radio.si/api/radiostations/logo/aktual.svg", + "liveAudio": "http://live.radio.si/Aktual", + "liveVideo": "https://radio.serv.si/AktualTV/video.m3u8", + "poster": "https://cdn1.radio.si/900/screenaktual_90c0280a8.jpg", + "lastSongs": "http://data.radio.si/api/lastsongsxml/aktual/json", + "epg": null, + "defaultText": "", + "www": "https://radioaktual.si", + "mountPoints": [ + "Aktual" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+386158801430" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://radioaktual.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "http://www.youtube.com/user/raktual?sub_confirmation=1" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/raktual" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radioaktual/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705160", + "dabUser": "aktual", + "dabPass": "GB31GZd5st0M", + "dabDefaultImg": "http://media.radio.si/logo/dns/aktual/RadioAktual_DAB.jpg", + "small": false + }, + { + "id": "Veseljak", + "title": "Radio Veseljak", + "slogan": "Najboljša domača glasba", + "logo": "http://datacache.radio.si/api/radiostations/logo/veseljak.svg", + "liveAudio": "http://live.radio.si/Veseljak", + "liveVideo": "https://radio.serv.si/VeseljakGolicaTV/video.m3u8", + "poster": "https://cdn1.radio.si/900/screenveseljak_166218c26.jpg", + "lastSongs": "http://data.radio.si/api/lastsongsxml/veseljak/json", + "epg": null, + "defaultText": "www.veseljak.si", + "www": "https://veseljak.si/", + "mountPoints": [ + "Veseljak", + "VeseljakPO" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38615880110" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://veseljak.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/c/VESELJAKNAJBOLJSADOMACAGLASBA" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/RadioVeseljak" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/veseljak.si/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705166", + "dabUser": "veseljak", + "dabPass": "sLRDCAX9j3k2", + "dabDefaultImg": "http://media.radio.si/logo/dns/veseljak/RadioVeseljak_DAB.jpg", + "small": false + }, + { + "id": "Radio1Rock", + "title": "Radio 1 ROCK", + "slogan": "100% Rock", + "logo": "http://datacache.radio.si/api/radiostations/logo/radio1rock.svg", + "liveAudio": "http://live.radio.si/Radio1Rock", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/radio1rock/json", + "epg": "http://spored.radio.si/api/now/radio1rock", + "defaultText": "www.radio1rock.si", + "www": "https://radio1rock.si/", + "mountPoints": [ + "Radio1Rock" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38683879300" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.radio1rock.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/R1Rock" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/R1rock.si/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiobob?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61109", + "rpUid": "705162", + "dabUser": "radiobob", + "dabPass": "cjT24PpyVxit6", + "dabDefaultImg": "http://media.radio.si/logo/dns/radio1rock/320x240.png", + "small": false + }, + { + "id": "Radio80", + "title": "Radio 1 80-a", + "slogan": "Samo hiti 80-ih", + "logo": "http://datacache.radio.si/api/radiostations/logo/radio80.svg", + "liveAudio": "http://live.radio.si/Radio80", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/radio80/json", + "epg": "http://spored.radio.si/api/now/radio80", + "defaultText": "www.radio80.si", + "www": "https://radio80.si/", + "mountPoints": [ + "Radio80" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38615008875" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://radio80.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/radio1slovenia" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/radioena" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radio180-a?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=89760", + "rpUid": "705102", + "dabUser": "radio80", + "dabPass": "nc6da2LolcBXC", + "dabDefaultImg": "http://media.radio.si/logo/dns/radio80/320x240.png", + "small": false + }, + { + "id": "Radio90", + "title": "Radio 1 90-a", + "slogan": "Samo hiti 90-ih", + "logo": "http://datacache.radio.si/api/radiostations/logo/radio90.svg", + "liveAudio": "http://live.radio.si/Radio90", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/radio90/json", + "epg": null, + "defaultText": "www.radio1.si", + "www": "https://radio1.si/", + "mountPoints": [ + "Radio90" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38615008875" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.radio1.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/radio1slovenia" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/radioena" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705172", + "dabUser": "radio90", + "dabPass": "P2RyUrHcyq7M", + "dabDefaultImg": "http://media.radio.si/logo/dns/radio90/320x240.png", + "small": false + }, + { + "id": "Toti", + "title": "Toti radio", + "slogan": "Toti hudi hiti", + "logo": "http://datacache.radio.si/api/radiostations/logo/toti.svg", + "liveAudio": "http://live.radio.si/Toti", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/toti/json", + "epg": "http://spored.radio.si/api/now/toti", + "defaultText": "www.totiradio.si", + "www": "https://totiradio.si/", + "mountPoints": [ + "Maxi", + "Toti" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38651220220" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://totiradio.si/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/totiradio?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=91414", + "rpUid": "705108", + "dabUser": "toti", + "dabPass": "wmAos05tECsmf", + "dabDefaultImg": "http://media.radio.si/logo/dns/toti/320x240.png", + "small": false + }, + { + "id": "Antena", + "title": "Radio Antena", + "slogan": "Največ hitov, najmanj govora", + "logo": "http://datacache.radio.si/api/radiostations/logo/antena.svg", + "liveAudio": "http://live.radio.si/Antena", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/antena/json", + "epg": "http://spored.radio.si/api/now/antena", + "defaultText": "www.radioantena.si", + "www": "https://radioantena.si/", + "mountPoints": [ + "Antena" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38612425630 " + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://radioantena.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/user/radioantenaslo" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/HitradioAntena" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radioantena.si/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radioantena?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=37864", + "rpUid": "705161", + "dabUser": "radioantena", + "dabPass": "nGkMhFk77jnBQ", + "dabDefaultImg": "http://media.radio.si/logo/dns/antena/320x240.png", + "small": false + }, + { + "id": "BestFM", + "title": "BestFM", + "slogan": "Muska, muska, muska", + "logo": "http://datacache.radio.si/api/radiostations/logo/bestfm.svg", + "liveAudio": "http://live.radio.si/BestFM", + "liveVideo": "https://radio.serv.si/BestTV/video.m3u8", + "poster": "https://cdn1.radio.si/900/screenbest_6559e3ac8.jpg", + "lastSongs": "http://data.radio.si/api/lastsongsxml/bestfm/json", + "epg": null, + "defaultText": "www.bestfm.si", + "www": "https://bestfm.si/", + "mountPoints": [ + "BestFM" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38673372030" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://bestfm.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/profile.php?id=100086776586975" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/bestfm.si/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705115", + "dabUser": "bestfm", + "dabPass": "momo911x", + "dabDefaultImg": "http://media.radio.si/logo/dns/bestfm/BestFM_DAB.jpg", + "small": false + }, + { + "id": "Krka", + "title": "Radio Krka", + "slogan": "Dolenjska v srcu", + "logo": "http://datacache.radio.si/api/radiostations/logo/krka.svg", + "liveAudio": "http://live.radio.si/Krka", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/krka/json", + "epg": "", + "defaultText": "www.radiokrka.si", + "www": "https://radiokrka.si/", + "mountPoints": [ + "Krka" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38673372030" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://radiokrka.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/user/radiokrka" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/radiokrka" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radiokrka/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705120", + "dabUser": "krka", + "dabPass": "qBi6z!um2Gm", + "dabDefaultImg": "http://media.radio.si/logo/dns/krka/RadioKrka_DAB.jpg", + "small": false + }, + { + "id": "Klasik", + "title": "Klasik radio", + "slogan": "Glasba, ki vas sprosti", + "logo": "https://data.radio.si/api/radiostations/logo/klasik.svg", + "liveAudio": "http://live.radio.si/Klasik", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/klasik/json", + "epg": "", + "defaultText": "www.klasikradio.si", + "www": "https://www.klasikradio.si/", + "mountPoints": [ + "Klasik" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38612425630" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.klasikradio.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/profile.php?id=100064736766638" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705176", + "dabUser": "klasik", + "dabPass": "mQTpTR9XEbiF", + "dabDefaultImg": "http://media.radio.si/logo/dns/klasik/320x240.png", + "small": false + }, + { + "id": "Maxi", + "title": "Toti Maxi", + "slogan": "Sama dobra glasba", + "logo": "https://data.radio.si/api/radiostations/logo/maxi.svg", + "liveAudio": "http://live.radio.si/Maxi", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/toti/json", + "epg": "", + "defaultText": "www.totimaxi.si", + "www": "https://www.radiomaxi.si/", + "mountPoints": [ + "Maxi" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38631628444" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.radiomaxi.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/profile.php?id=100064736766638" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/radiosalomon" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radiosalomon/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/totiradio?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=37998", + "rpUid": "705109", + "dabUser": "salomon", + "dabPass": "a1bfadd8b8ut", + "dabDefaultImg": "http://media.radio.si/logo/dns/salomon/RadioSalomon_DAB.jpg", + "small": false + }, + { + "id": "Salomon", + "title": "Radio Salomon", + "slogan": "Izbrana urbana glasba", + "logo": "http://datacache.radio.si/api/radiostations/logo/salomon.svg", + "liveAudio": "http://live.radio.si/Salomon", + "liveVideo": null, + "poster": null, + "lastSongs": "http://data.radio.si/api/lastsongsxml/salomon/json", + "epg": "", + "defaultText": "www.radiosalomon.si", + "www": "https://radiosalomon.si/", + "mountPoints": [ + "Salomon" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+386015880111" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://radiosalomon.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/channel/UCd7OpUbSIoZarJgwFf4aIxw" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/RadioSalomon" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radiosalomon/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705116", + "dabUser": "salomon", + "dabPass": "a1bfadd8b8ut", + "dabDefaultImg": "http://media.radio.si/logo/dns/salomon/RadioSalomon_DAB.jpg", + "small": false + }, + { + "id": "Ptuj", + "title": "Radio Ptuj", + "slogan": "Največje uspešnice vseh časov", + "logo": "https://data.radio.si/api/radiostations/logo/ptuj.svg", + "liveAudio": "http://live.radio.si/Ptuj", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/ptuj/json", + "epg": "", + "defaultText": "www.radio-ptuj.si", + "www": "https://www.radio-ptuj.si/", + "mountPoints": [ + "Ptuj" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38627493420" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.radio-ptuj.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/@RadioPtuj" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/RadioPtuj" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radio_ptuj/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705119", + "dabUser": "ptuj", + "dabPass": "cwv4jXVKMYT", + "dabDefaultImg": "http://media.radio.si/logo/dns/ptuj/RadioPtuj_DAB.jpg", + "small": false + }, + { + "id": "Fantasy", + "title": "Radio Fantasy", + "slogan": "Same dobre vibracije", + "logo": "https://data.radio.si/api/radiostations/logo/fantasy.svg", + "liveAudio": "http://live.radio.si/Fantasy", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/fantasy/json", + "epg": "http://spored.radio.si/api/now/robin", + "defaultText": "", + "www": "https://rfantasy.si/", + "mountPoints": [ + "Fantasy" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38634903921" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.rfantasy.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/c/RadioFantasyTv" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/RadioFantasySlo" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radiofantasyslo/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiofantasy?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61118", + "rpUid": "", + "dabUser": "radiorobin", + "dabPass": "rt5mo9b9", + "dabDefaultImg": "http://media.radio.si/logo/dns/robin/320x240.png", + "small": false + }, + { + "id": "Robin", + "title": "Radio Robin", + "slogan": "Brez tebe ni mene", + "logo": "https://data.radio.si/api/radiostations/logo/robin.svg", + "liveAudio": "http://live.radio.si/Robin", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/robin/json", + "epg": "http://spored.radio.si/api/now/robin", + "defaultText": "www.robin.si", + "www": "https://www.robin.si/", + "mountPoints": [ + "Robin" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38653302822" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.robin.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/channel/UCACfPObotnJAnVXfCZNMlUg" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/Radio.Robin.goriski" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/radio_robin/" + } + ], + "enabled": true, + "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiorobin?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=37984", + "rpUid": "705103", + "dabUser": "radiorobin", + "dabPass": "rt5mo9b9", + "dabDefaultImg": "http://media.radio.si/logo/dns/robin/320x240.png", + "small": false + }, + { + "id": "Koroski", + "title": "Koroški radio", + "slogan": "Ritem Koroške", + "logo": "https://data.radio.si/api/radiostations/logo/koroski.svg", + "liveAudio": "http://live.radio.si/Koroski", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/koroski/json", + "epg": "http://spored.radio.si/api/now/koroski", + "defaultText": "www.koroski-radio.si", + "www": "https://www.koroski-radio.si/", + "mountPoints": [ + "Koroski" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38628841245" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://www.koroski-radio.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/channel/UCLwH6lX4glK4o1N77JkeaJw" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/KoroskiRadio" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/koroski_r/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705105", + "dabUser": "koroski", + "dabPass": "num87dhket", + "dabDefaultImg": "http://media.radio.si/logo/dns/koroski/320x240.png", + "small": true + }, + { + "id": "VeseljakZlatiZvoki", + "title": "Veseljak Zlati zvoki", + "slogan": "Najvecja zakladnica slovenske domace glasbe", + "logo": "https://data.radio.si/api/radiostations/logo/veseljakzlatizvoki.svg", + "liveAudio": "http://live.radio.si/VeseljakZlatiZvoki", + "liveVideo": null, + "poster": null, + "lastSongs": "https://data.radio.si/api/lastsongsxml/veseljakzlatizvoki/json", + "epg": "", + "defaultText": "www.veseljak.si", + "www": "https://www.veseljak.si/", + "mountPoints": [ + "VeseljakZlatiZvoki" + ], + "social": [ + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg", + "link": "tel:+38615880110" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg", + "link": "https://veseljak.si/" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg", + "link": "https://www.youtube.com/c/VESELJAKNAJBOLJSADOMACAGLASBA" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg", + "link": "https://www.facebook.com/RadioVeseljak" + }, + { + "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg", + "link": "https://www.instagram.com/veseljak.si/" + } + ], + "enabled": true, + "radioApiIO": "", + "rpUid": "705175", + "dabUser": "zlatizvoki", + "dabPass": "4jeeUnjA4qYV", + "dabDefaultImg": "http://media.radio.si/logo/dns/veseljakzlatizvoki/RadioVeseljakZlatiZvoki_DAB.jpg", + "small": false + }, + { + "id": "RockMB", + "title": "Rock Maribor", + "slogan": "100% Rock", + "logo": "https://data.radio.si/api/radiostations/logo/rockmb.svg", + "liveAudio": "http://live.radio.si/RockMB", + "liveVideo": null, diff --git a/webapp/styles.css b/webapp/styles.css new file mode 100644 index 0000000..1748b27 --- /dev/null +++ b/webapp/styles.css @@ -0,0 +1,886 @@ +/* Copied from src/styles.css */ +:root { + --bg-gradient: linear-gradient(135deg, #7b7fd8, #b57cf2); + --glass-bg: rgba(255, 255, 255, 0.1); + --glass-border: rgba(255, 255, 255, 0.2); + --accent: #dfa6ff; + --accent-glow: rgba(223, 166, 255, 0.5); + --text-main: #ffffff; + --text-muted: rgba(255, 255, 255, 0.7); + --danger: #cf6679; + --success: #7dffb3; + --card-radius: 10px; +} + +* { + box-sizing: border-box; + user-select: none; + -webkit-user-drag: none; + cursor: default; +} + +/* Hide Scrollbars */ +::-webkit-scrollbar { + display: none; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + width: 100vw; + background: linear-gradient(-45deg, #7b7fd8, #b57cf2, #8b5cf6, #6930c3, #7b7fd8); +.status-indicator-wrap { + display:flex; + align-items:center; + gap:10px; + justify-content:center; + margin-top:8px; + color:var(--text-main); +} + background-size: 400% 400%; + animation: gradientShift 12s ease-in-out infinite; + font-family: 'Segoe UI', system-ui, sans-serif; + color: var(--text-main); + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} + +@keyframes gradientShift { + 0% { + background-position: 0% 50%; + } + 25% { + background-position: 100% 50%; + } + 50% { + background-position: 50% 100%; + } + 75% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +/* Background Blobs */ +.bg-shape { + position: absolute; + border-radius: 50%; + filter: blur(60px); + z-index: 0; + opacity: 0.6; + animation: float 10s infinite alternate; +} + +.shape-1 { + width: 300px; + height: 300px; + background: #5e60ce; + top: -50px; + left: -50px; +} + +.shape-2 { + width: 250px; + height: 250px; + background: #ff6bf0; + bottom: -50px; + right: -50px; + animation-delay: -5s; +} + +@keyframes float { + 0% { transform: translate(0, 0); } + 100% { transform: translate(30px, 30px); } +} + +.app-container { + width: 100%; + height: 100%; + position: relative; + padding: 10px; /* Slight padding from window edges if desired, or 0 */ +} + +.glass-card { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + backdrop-filter: blur(24px); + border-radius: var(--card-radius); + display: flex; + flex-direction: column; + padding: 24px; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); +} + +/* Make whole card draggable for window movement; interactive children override with no-drag */ +.glass-card { + -webkit-app-region: drag; +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + -webkit-app-region: drag; /* Draggable area */ + padding: 10px 14px 8px 14px; + border-radius: 14px; + background: linear-gradient(135deg, rgba(60,84,255,0.14), rgba(123,127,216,0.10)); + border: 1px solid rgba(120,130,255,0.12); + box-shadow: 0 10px 30px rgba(28,25,60,0.35), inset 0 1px 0 rgba(255,255,255,0.03); + backdrop-filter: blur(8px) saturate(120%); + position: relative; + z-index: 3; +} + +.header-top { + display:flex; + justify-content:space-between; + align-items:center; + width:100%; +} + + +.header-top-row { + display:flex; + justify-content:space-between; + align-items:center; + width:100%; +} + + +.header-icons-left { flex: 0 0 auto; display:flex; align-items:center; gap:8px; padding-left:8px; } + +.header-center-status { flex:1; display:flex; justify-content:center; align-items:center; } + +.header-close { flex:0 0 auto; } + +.header-second-row { + display:flex; + justify-content:center; + align-items:center; + width:100%; + margin-top:6px; +} + +.status-indicator-wrap { display:flex; gap:8px; align-items:center; color:var(--text-main); } + +.header-third-row { display:none; } +.header-left { + justify-content: flex-start; + flex: 0 0 auto; +} + +.header-right { + justify-content: flex-end; + flex: 0 0 auto; +} + +.app-title { text-align: center; } + +.header-info { + text-align: center; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.app-title { + font-weight: 700; + font-size: 1.05rem; + color: var(--text-main); + letter-spacing: 0.4px; +} + +.status-indicator { + font-size: 0.85rem; + color: var(--success); + margin-top: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.status-dot { + width: 6px; + height: 6px; + background-color: var(--success); + border-radius: 50%; + box-shadow: 0 0 8px var(--success); +} + +.icon-btn { + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.03); + color: var(--text-main); + padding: 8px; + cursor: pointer; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; + -webkit-app-region: no-drag; /* Buttons clickable */ +} + +.icon-btn:hover { + background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); + transform: translateY(-3px); + box-shadow: 0 10px 24px rgba(0,0,0,0.2); +} + +.header-buttons { + display: flex; + gap: 8px; + align-items: center; + -webkit-app-region: no-drag; +} + +.close-btn:hover { + background: rgba(207, 102, 121, 0.3) !important; + color: var(--danger); +} + +/* Artwork */ +.artwork-section { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +.artwork-container { + width: 220px; + height: 220px; + border-radius: 24px; + padding: 6px; /* spacing for ring */ + background: linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.00)); + box-shadow: 0 12px 40px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); + backdrop-filter: blur(8px) saturate(120%); + position: relative; +} + +.artwork-placeholder { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #4ea8de, #6930c3); + border-radius: 20px; + display: flex; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; + box-shadow: inset 0 0 30px rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.04); +} + +/* glossy inner rim for artwork */ +.artwork-container::after { + content: ''; + position: absolute; + inset: 6px; /* follows padding to create rim */ + border-radius: 20px; + pointer-events: none; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), inset 0 -20px 40px rgba(255,255,255,0.02); + mix-blend-mode: overlay; +} + +/* Make artwork clickable and give subtle hover feedback */ +.artwork-placeholder { + cursor: pointer; + transition: transform 0.12s ease, box-shadow 0.12s ease; +} +.artwork-placeholder:hover { + box-shadow: 0 18px 40px rgba(255, 255, 0, 0.45), inset 0 0 28px rgba(255,255,255,0.02); +} + +.artwork-placeholder { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #4ea8de, #6930c3); + border-radius: 20px; + display: flex; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; + box-shadow: inset 0 0 20px rgba(0,0,0,0.2); +} + +.station-logo-text { + font-size: 5rem; + font-weight: 800; + font-style: italic; + color: rgba(255,255,255,0.9); + text-shadow: 0 4px 10px rgba(0,0,0,0.3); + position: relative; + z-index: 3; +} + +.station-logo-img { + /* Fill the artwork placeholder while keeping aspect ratio and inner padding */ + width: 100%; + height: 100%; + object-fit: contain; + display: block; + padding: 12px; /* inner spacing from rounded edges */ + box-sizing: border-box; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0,0,0,0.35); + position: relative; + z-index: 3; +} + +/* Logo blobs container sits behind logo but inside artwork placeholder */ +.logo-blobs { + position: absolute; + inset: 0; + filter: url(#goo); + z-index: 1; + pointer-events: none; +} + +.blob { + position: absolute; + border-radius: 50%; + /* more transparent overall */ + opacity: 0.18; + /* slightly smaller blur for subtle definition */ + filter: blur(6px); +} + +.b1 { width: 110px; height: 110px; left: 8%; top: 20%; background: radial-gradient(circle at 30% 30%, #c77dff, #8b5cf6); animation: float1 6s ease-in-out infinite; } +.b2 { width: 85px; height: 85px; right: 6%; top: 10%; background: radial-gradient(circle at 30% 30%, #7bffd1, #7dffb3); animation: float2 5.5s ease-in-out infinite; } +.b3 { width: 95px; height: 95px; left: 20%; bottom: 12%; background: radial-gradient(circle at 20% 20%, #ffd07a, #ff6bf0); animation: float3 7s ease-in-out infinite; } +.b4 { width: 70px; height: 70px; right: 24%; bottom: 18%; background: radial-gradient(circle at 30% 30%, #6bd3ff, #4ea8de); animation: float4 6.5s ease-in-out infinite; } +.b5 { width: 50px; height: 50px; left: 46%; top: 36%; background: radial-gradient(circle at 40% 40%, #ffa6d6, #c77dff); animation: float5 8s ease-in-out infinite; } + +/* Additional blobs */ +.b6 { width: 75px; height: 75px; left: 12%; top: 48%; background: radial-gradient(circle at 30% 30%, #bde7ff, #6bd3ff); animation: float6 6.8s ease-in-out infinite; } +.b7 { width: 42px; height: 42px; right: 10%; top: 42%; background: radial-gradient(circle at 40% 40%, #ffd9b3, #ffd07a); animation: float7 7.2s ease-in-out infinite; } +.b8 { width: 70px; height: 70px; left: 34%; bottom: 8%; background: radial-gradient(circle at 30% 30%, #e3b6ff, #c77dff); animation: float8 6.4s ease-in-out infinite; } +.b9 { width: 36px; height: 36px; right: 34%; bottom: 6%; background: radial-gradient(circle at 30% 30%, #9ef7d3, #7bffd1); animation: float9 8.4s ease-in-out infinite; } +.b10 { width: 30px; height: 30px; left: 52%; bottom: 28%; background: radial-gradient(circle at 30% 30%, #ffd0f0, #ffa6d6); animation: float10 5.8s ease-in-out infinite; } + +@keyframes float1 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(12px) translateX(8px) scale(1.06); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float2 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-10px) translateX(-6px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float3 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(8px) translateX(-10px) scale(1.05); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float4 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-6px) translateX(10px) scale(1.03); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float5 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-12px) translateX(4px) scale(1.07); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float6 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-8px) translateX(6px) scale(1.05); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float7 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(10px) translateX(-6px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float8 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-6px) translateX(10px) scale(1.03); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float9 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(12px) translateX(-4px) scale(1.06); } 100% { transform: translateY(0) translateX(0) scale(1); } } +@keyframes float10 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-10px) translateX(2px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } } + +/* Slightly darken backdrop gradient so blobs read better */ +.artwork-placeholder::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(0,0,0,0.06), rgba(0,0,0,0.12)); + z-index: 0; +} + +/* Make artwork/logo clickable: show pointer cursor */ +.artwork-placeholder, +.artwork-placeholder:hover, +.station-logo-img, +.station-logo-text { + cursor: pointer !important; + pointer-events: auto; +} + +/* Subtle hover affordance to make clickability clearer */ +.artwork-placeholder:hover .station-logo-img, +.artwork-placeholder:hover .station-logo-text { + transform: scale(1.03); + transition: transform 160ms ease; +} + +/* Track Info */ +.track-info { + text-align: center; + margin-bottom: 20px; + /* Reserve fixed space for station name, artist and title to avoid layout jumps */ + min-height: 5.2rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.track-info h2 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + text-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Now playing container: artist and title on separate lines */ +#now-playing { + margin: 6px 0 0; + width: 100%; + /* Reserve two lines so content changes don't shift layout */ + height: 2.6rem; + display: block; +} + +#now-playing .now-artist, +#now-playing .now-title { + color: var(--text-main); + font-size: 0.95rem; + font-weight: 600; + line-height: 1.2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Hide visually but keep layout space */ +#now-playing.hidden { + visibility: hidden; +} + +.track-info p { + margin: 6px 0 0; + color: var(--text-muted); + font-size: 0.95rem; +} + +/* Progress Bar (Visual) */ +.progress-container { + width: 100%; + height: 4px; + background: rgba(255,255,255,0.1); + border-radius: 2px; + margin-bottom: 30px; + position: relative; +} + +.progress-fill { + width: 100%; /* Live always full or pulsing */ + height: 100%; + background: linear-gradient(90deg, var(--accent), #fff); + border-radius: 2px; + opacity: 0.8; + box-shadow: 0 0 10px var(--accent-glow); +} + +.progress-handle { + position: absolute; + right: 0; + top: 50%; + transform: translate(50%, -50%); + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 10px rgba(255,255,255,0.8); +} + +/* Controls */ +.controls-section { + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + margin-bottom: 30px; +} + +.control-btn { + background: none; + border: none; + color: var(--text-main); + cursor: pointer; + transition: transform 0.1s, opacity 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.control-btn:active { + transform: scale(0.9); +} + +.control-btn.secondary { + width: 48px; + height: 48px; + border-radius: 50%; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + box-shadow: 0 4px 10px rgba(0,0,0,0.1); +} + +.control-btn.primary { + width: 72px; + height: 72px; + border-radius: 50%; + background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05)); + border: 1px solid rgba(255,255,255,0.3); + box-shadow: 0 8px 20px rgba(0,0,0,0.2), inset 0 0 10px rgba(255,255,255,0.1); + color: #fff; +} + +.control-btn.primary svg { + filter: drop-shadow(0 0 5px var(--accent-glow)); +} + +/* Playing state - pulsing glow ring */ +.control-btn.primary.playing { + animation: pulse-ring 2s ease-in-out infinite; +} + +@keyframes pulse-ring { + 0%, 100% { + box-shadow: 0 8px 20px rgba(0,0,0,0.2), + inset 0 0 10px rgba(255,255,255,0.1), + 0 0 0 0 rgba(223, 166, 255, 0.7); + } + 50% { + box-shadow: 0 8px 20px rgba(0,0,0,0.2), + inset 0 0 10px rgba(255,255,255,0.1), + 0 0 0 8px rgba(223, 166, 255, 0); + } +} + +/* Icon container prevents layout jump */ +.icon-container { + position: relative; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-container svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.hidden { + display: none !important; +} + +/* Volume */ +.volume-section { + display: flex; + align-items: center; + gap: 12px; + margin-top: auto; + padding: 0 10px; +} + +.slider-container { + flex: 1; +} + +/* Make slider interactive when the parent card is draggable */ +.slider-container, +input[type=range] { + -webkit-app-region: no-drag; +} + +input[type=range] { + width: 100%; + background: transparent; + -webkit-appearance: none; + appearance: none; +} + +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: rgba(255,255,255,0.2); + border-radius: 2px; +} + +input[type=range]::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 50%; + background: #ffffff; + cursor: pointer; + -webkit-appearance: none; + margin-top: -6px; /* align with track */ + box-shadow: 0 0 10px rgba(0,0,0,0.2); +} + +#volume-value { + font-size: 0.8rem; + font-weight: 500; + width: 30px; + text-align: right; +} + +.icon-btn.small { + padding: 0; + width: 24px; + height: 24px; +} + +/* Cast Overlay (Beautified as per layout2_plan.md) */ +.overlay { + position: fixed; + inset: 0; + background: rgba(20, 10, 35, 0.45); + backdrop-filter: blur(14px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; +} + +.overlay:not(.hidden) { + opacity: 1; + pointer-events: auto; +} + +/* Modal */ +.modal { + width: min(420px, calc(100vw - 48px)); + padding: 22px; + border-radius: 22px; + background: rgba(30, 30, 40, 0.82); + border: 1px solid rgba(255,255,255,0.12); + box-shadow: 0 30px 80px rgba(0,0,0,0.6); + color: #fff; + animation: pop 0.22s ease; + -webkit-app-region: no-drag; +} + +@keyframes pop { + from { transform: scale(0.94); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.modal h2 { + margin: 0 0 14px; + text-align: center; + font-size: 20px; +} + +/* Device list */ +.device-list { + list-style: none; + padding: 10px 5px; + margin: 0 0 18px; + max-height: 360px; + overflow-y: auto; +} + +/* Stations grid to show cards (used for stations overlay) */ +.stations-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; + padding: 8px; +} + +.station-card { + list-style: none; + padding: 12px; + border-radius: 14px; + cursor: pointer; + background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); + border: 1px solid rgba(255,255,255,0.06); + display: flex; + gap: 12px; + align-items: center; + transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s; +} + +.station-card:hover { + transform: translateY(-6px); + box-shadow: 0 18px 40px rgba(0,0,0,0.45); +} + +.station-card.selected { + background: linear-gradient(135deg, #c77dff, #8b5cf6); + color: #111; + box-shadow: 0 10px 30px rgba(199,125,255,0.22); +} + +.station-card-left { + width: 56px; + height: 56px; + flex: 0 0 56px; + display:flex; + align-items:center; + justify-content:center; +} + +.station-card-logo { + width: 56px; + height: 56px; + object-fit:contain; + border-radius: 10px; + box-shadow: 0 6px 18px rgba(0,0,0,0.35); + background: rgba(255,255,255,0.02); +} + +.station-card-fallback { + width: 56px; + height: 56px; + border-radius: 10px; + display:flex; + align-items:center; + justify-content:center; + font-weight:800; + font-size:1.2rem; + background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); + color: var(--text-main); +} + +.station-card-body { + display:flex; + flex-direction:column; + gap:3px; + overflow:hidden; +} + +.station-card-title { + font-weight:700; + font-size:0.95rem; + line-height:1.1; +} + +.station-card-sub { + font-size:0.8rem; + color: rgba(255,255,255,0.7); + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; +} + +/* Device row */ +.device { + padding: 12px 14px; + border-radius: 14px; + margin-bottom: 8px; + cursor: pointer; + background: rgba(255,255,255,0.05); + transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; + text-align: left; +} + +.device:hover { + background: rgba(255,255,255,0.10); + transform: translateY(-1px); +} + +.device .device-main { + font-size: 15px; + font-weight: 600; + color: var(--text-main); +} + +.device .device-sub { + margin-top: 3px; + font-size: 12px; + opacity: 0.7; + color: var(--text-muted); +} + +/* Selected device */ +.device.selected { + background: linear-gradient(135deg, #c77dff, #8b5cf6); + box-shadow: 0 0 18px rgba(199,125,255,0.65); + color: #111; +} + +.device.selected .device-main, +.device.selected .device-sub { + color: #111; +} + +.device.selected .device-sub { + opacity: 0.85; +} + +/* Cancel button */ +.btn.cancel { + width: 100%; + padding: 12px; + border-radius: 999px; + border: none; + background: #d16b7d; + color: #fff; + font-size: 15px; + cursor: pointer; + transition: transform 0.15s ease, background 0.2s; + font-weight: 600; +} + +.btn.cancel:hover { + transform: scale(1.02); + background: #e17c8d; +} + +/* Editor specific tweaks */ +.modal form input { + outline: none; +} + +/* Ensure editor overlay input fields look consistent */ +#editor-list .device { + display: block; +} + +.btn.edit-btn, .btn.delete-btn { + padding: 8px 10px; + border-radius: 10px; + border: none; + color: #fff; + font-weight: 700; + cursor: pointer; +} + +#add-station-form button.btn { + border-radius: 10px; +} + +/* Make modal form inputs visible on dark translucent background */ +.modal input, +.modal textarea, +.modal select { + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.12); + color: var(--text-main); + padding: 10px 12px; + border-radius: 8px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); +} + +.modal input::placeholder, +.modal textarea::placeholder { + color: rgba(255,255,255,0.55); +} + +.btn { + padding: 10px 14px; + border-radius: 10px; + border: none; + cursor: pointer; + font-weight: 700; +} diff --git a/webapp/sw.js b/webapp/sw.js new file mode 100644 index 0000000..fa201a5 --- /dev/null +++ b/webapp/sw.js @@ -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' }); + }); + }) + ); +}); diff --git a/webapp/vite.config.js b/webapp/vite.config.js new file mode 100644 index 0000000..4d94e34 --- /dev/null +++ b/webapp/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +// Allow Vite dev server to read files from parent folder so we can import +// the existing `src` code without copying it. +export default defineConfig({ + server: { + fs: { + // allow access to parent workspace root + allow: [path.resolve(__dirname, '..')] + } + } +});