diff --git a/android/README.md b/android/README.md
new file mode 100644
index 0000000..5e361fc
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,11 @@
+This folder is not a full Android Studio project.
+
+The buildable Android Studio/Gradle project is generated by Tauri at:
+
+- src-tauri/gen/android
+
+If you haven't generated it yet, run from the repo root:
+
+- .\node_modules\.bin\tauri.cmd android init --ci
+
+Then open `src-tauri/gen/android` in Android Studio and build the APK/AAB.
diff --git a/android/app/src/main/assets/assets/appIcon.png b/android/app/src/main/assets/assets/appIcon.png
new file mode 100644
index 0000000..d789348
Binary files /dev/null and b/android/app/src/main/assets/assets/appIcon.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io.zip b/android/app/src/main/assets/assets/favicon_io.zip
new file mode 100644
index 0000000..15adffb
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io.zip differ
diff --git a/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png b/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png
new file mode 100644
index 0000000..4757781
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png b/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png
new file mode 100644
index 0000000..8f1299a
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/app-icon.png b/android/app/src/main/assets/assets/favicon_io/app-icon.png
new file mode 100644
index 0000000..8f1299a
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/app-icon.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png b/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png
new file mode 100644
index 0000000..f29ebcf
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png b/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png
new file mode 100644
index 0000000..4eb99da
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png b/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png
new file mode 100644
index 0000000..73c330b
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png differ
diff --git a/android/app/src/main/assets/assets/favicon_io/icon.ico b/android/app/src/main/assets/assets/favicon_io/icon.ico
new file mode 100644
index 0000000..586e969
Binary files /dev/null and b/android/app/src/main/assets/assets/favicon_io/icon.ico differ
diff --git a/android/app/src/main/assets/assets/favicon_io/site.webmanifest b/android/app/src/main/assets/assets/favicon_io/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/android/app/src/main/assets/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"}
\ No newline at end of file
diff --git a/android/app/src/main/assets/assets/javascript.svg b/android/app/src/main/assets/assets/javascript.svg
new file mode 100644
index 0000000..f9abb2b
--- /dev/null
+++ b/android/app/src/main/assets/assets/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/android/app/src/main/assets/assets/tauri.svg b/android/app/src/main/assets/assets/tauri.svg
new file mode 100644
index 0000000..0c0e6aa
--- /dev/null
+++ b/android/app/src/main/assets/assets/tauri.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/android/app/src/main/assets/index.html b/android/app/src/main/assets/index.html
new file mode 100644
index 0000000..3ea45e7
--- /dev/null
+++ b/android/app/src/main/assets/index.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+ Radio1 Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+ Radio 1 MB
+ Live Stream
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose
+
+
+
+
+ Scanning...
+ Searching for speakers
+
+
+
+
Cancel
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/assets/main.js b/android/app/src/main/assets/main.js
new file mode 100644
index 0000000..50c6b53
--- /dev/null
+++ b/android/app/src/main/assets/main.js
@@ -0,0 +1,355 @@
+const { invoke } = window.__TAURI__.core;
+const { getCurrentWindow } = window.__TAURI__.window;
+
+// State
+let stations = [];
+let currentIndex = 0;
+let isPlaying = false;
+let currentMode = 'local'; // 'local' | 'cast'
+let currentCastDevice = null;
+const audio = new Audio();
+
+// UI Elements
+const stationNameEl = document.getElementById('station-name');
+const stationSubtitleEl = document.getElementById('station-subtitle');
+const statusTextEl = document.getElementById('status-text');
+const statusDotEl = document.querySelector('.status-dot');
+const playBtn = document.getElementById('play-btn');
+const iconPlay = document.getElementById('icon-play');
+const iconStop = document.getElementById('icon-stop');
+const prevBtn = document.getElementById('prev-btn');
+const nextBtn = document.getElementById('next-btn');
+const volumeSlider = document.getElementById('volume-slider');
+const volumeValue = document.getElementById('volume-value');
+const castBtn = document.getElementById('cast-toggle-btn');
+const castOverlay = document.getElementById('cast-overlay');
+const closeOverlayBtn = document.getElementById('close-overlay');
+const deviceListEl = document.getElementById('device-list');
+const logoTextEl = document.querySelector('.station-logo-text');
+const logoImgEl = document.getElementById('station-logo-img');
+
+// Init
+async function init() {
+ await loadStations();
+ setupEventListeners();
+ updateUI();
+}
+
+async function loadStations() {
+ try {
+ 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`.
+ stations = raw
+ .map((s) => {
+ // If already in the old format, keep as-is
+ if (s.name && s.url) return s;
+
+ const name = s.title || s.id || s.name || 'Unknown';
+ // Prefer liveAudio, fall back to liveVideo or any common fields
+ const url = s.liveAudio || s.liveVideo || s.liveStream || s.url || '';
+
+ return {
+ id: s.id || name,
+ name,
+ url,
+ logo: s.logo || s.poster || '',
+ enabled: typeof s.enabled === 'boolean' ? s.enabled : true,
+ raw: s,
+ };
+ })
+ // Filter out disabled stations and those without a stream URL
+ .filter((s) => s.enabled !== false && s.url && s.url.length > 0);
+
+ if (stations.length > 0) {
+ currentIndex = 0;
+ loadStation(currentIndex);
+ }
+ } catch (e) {
+ console.error('Failed to load stations', e);
+ statusTextEl.textContent = 'Error loading stations';
+ }
+}
+
+function setupEventListeners() {
+ playBtn.addEventListener('click', togglePlay);
+ prevBtn.addEventListener('click', playPrev);
+ nextBtn.addEventListener('click', playNext);
+
+ volumeSlider.addEventListener('input', handleVolumeInput);
+
+ castBtn.addEventListener('click', openCastOverlay);
+ closeOverlayBtn.addEventListener('click', closeCastOverlay);
+
+ // Close overlay on background click
+ castOverlay.addEventListener('click', (e) => {
+ if (e.target === castOverlay) closeCastOverlay();
+ });
+
+ // Close button
+ document.getElementById('close-btn').addEventListener('click', async () => {
+ const appWindow = getCurrentWindow();
+ await appWindow.close();
+ });
+
+ // Menu button - explicit functionality or placeholder?
+ // For now just log or maybe show about
+ document.getElementById('menu-btn').addEventListener('click', () => {
+ openStationsOverlay();
+ });
+
+ // Hotkeys?
+}
+
+function loadStation(index) {
+ if (index < 0 || index >= stations.length) return;
+ const station = stations[index];
+
+ stationNameEl.textContent = station.name;
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
+
+ // Update Logo Text (First letter or number)
+ // Simple heuristic: if name has a number, use it, else first letter
+ // If station has a logo URL, show the image; otherwise show the text fallback
+ if (station.logo && station.logo.length > 0) {
+ logoImgEl.src = station.logo;
+ logoImgEl.classList.remove('hidden');
+ logoTextEl.classList.add('hidden');
+ } else {
+ // Fallback to single-letter/logo text
+ logoImgEl.src = '';
+ logoImgEl.classList.add('hidden');
+ const numberMatch = station.name.match(/\d+/);
+ if (numberMatch) {
+ logoTextEl.textContent = numberMatch[0];
+ } else {
+ logoTextEl.textContent = station.name.charAt(0).toUpperCase();
+ }
+ logoTextEl.classList.remove('hidden');
+ }
+}
+
+async function togglePlay() {
+ if (isPlaying) {
+ await stop();
+ } else {
+ await play();
+ }
+}
+
+async function play() {
+ const station = stations[currentIndex];
+ if (!station) return;
+
+ statusTextEl.textContent = 'Buffering...';
+ statusDotEl.style.backgroundColor = 'var(--text-muted)'; // Grey/Yellow while loading
+
+ if (currentMode === 'local') {
+ audio.src = station.url;
+ audio.volume = volumeSlider.value / 100;
+ try {
+ await audio.play();
+ isPlaying = true;
+ updateUI();
+ } catch (e) {
+ console.error('Playback failed', e);
+ statusTextEl.textContent = 'Error';
+ }
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ // Cast logic
+ try {
+ await invoke('cast_play', { deviceName: currentCastDevice, url: station.url });
+ isPlaying = true;
+ // Sync volume
+ const vol = volumeSlider.value / 100;
+ invoke('cast_set_volume', { deviceName: currentCastDevice, volume: vol });
+ updateUI();
+ } catch (e) {
+ console.error('Cast failed', e);
+ statusTextEl.textContent = 'Cast Error';
+ currentMode = 'local'; // Fallback
+ updateUI();
+ }
+ }
+}
+
+async function stop() {
+ if (currentMode === 'local') {
+ audio.pause();
+ audio.src = '';
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ try {
+ await invoke('cast_stop', { deviceName: currentCastDevice });
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ isPlaying = false;
+ updateUI();
+}
+
+async function playNext() {
+ if (stations.length === 0) return;
+
+ // If playing, stop first? Or seamless?
+ // For radio, seamless switch requires stop then play new URL
+ const wasPlaying = isPlaying;
+
+ if (wasPlaying) await stop();
+
+ currentIndex = (currentIndex + 1) % stations.length;
+ loadStation(currentIndex);
+
+ if (wasPlaying) await play();
+}
+
+async function playPrev() {
+ if (stations.length === 0) return;
+
+ const wasPlaying = isPlaying;
+
+ if (wasPlaying) await stop();
+
+ currentIndex = (currentIndex - 1 + stations.length) % stations.length;
+ loadStation(currentIndex);
+
+ if (wasPlaying) await play();
+}
+
+function updateUI() {
+ // Play/Stop Button
+ if (isPlaying) {
+ iconPlay.classList.add('hidden');
+ iconStop.classList.remove('hidden');
+ playBtn.classList.add('playing'); // Add pulsing ring animation
+ statusTextEl.textContent = 'Playing';
+ statusDotEl.style.backgroundColor = 'var(--success)';
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
+ } else {
+ iconPlay.classList.remove('hidden');
+ iconStop.classList.add('hidden');
+ playBtn.classList.remove('playing'); // Remove pulsing ring
+ statusTextEl.textContent = 'Ready';
+ statusDotEl.style.backgroundColor = 'var(--text-muted)';
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Connected to ${currentCastDevice}` : 'Live Stream';
+ }
+}
+
+function handleVolumeInput() {
+ const val = volumeSlider.value;
+ volumeValue.textContent = `${val}%`;
+ const decimals = val / 100;
+
+ if (currentMode === 'local') {
+ audio.volume = decimals;
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ invoke('cast_set_volume', { deviceName: currentCastDevice, volume: decimals });
+ }
+}
+
+// Cast Logic
+async function openCastOverlay() {
+ castOverlay.classList.remove('hidden');
+ castOverlay.setAttribute('aria-hidden', 'false');
+ deviceListEl.innerHTML = 'Scanning...
Searching for speakers
';
+
+ try {
+ const devices = await invoke('list_cast_devices');
+ deviceListEl.innerHTML = '';
+
+ // Add "This Computer" option
+ const localLi = document.createElement('li');
+ localLi.className = 'device' + (currentMode === 'local' ? ' selected' : '');
+ localLi.innerHTML = 'This Computer
Local Playback
';
+ localLi.onclick = () => selectCastDevice(null);
+ deviceListEl.appendChild(localLi);
+
+ if (devices.length > 0) {
+ devices.forEach(d => {
+ const li = document.createElement('li');
+ li.className = 'device' + (currentMode === 'cast' && currentCastDevice === d ? ' selected' : '');
+ li.innerHTML = `${d}
Google Cast Speaker
`;
+ li.onclick = () => selectCastDevice(d);
+ deviceListEl.appendChild(li);
+ });
+ }
+ } catch (e) {
+ deviceListEl.innerHTML = `Error
${e}
`;
+ }
+}
+
+function closeCastOverlay() {
+ castOverlay.classList.add('hidden');
+ castOverlay.setAttribute('aria-hidden', 'true');
+}
+
+async function selectCastDevice(deviceName) {
+ closeCastOverlay();
+
+ // If checking same device, do nothing
+ if (deviceName === currentCastDevice) return;
+
+ // If switching mode, stop current playback
+ if (isPlaying) {
+ await stop();
+ }
+
+ if (deviceName) {
+ currentMode = 'cast';
+ currentCastDevice = deviceName;
+ castBtn.style.color = 'var(--success)';
+ } else {
+ currentMode = 'local';
+ currentCastDevice = null;
+ castBtn.style.color = 'var(--text-main)';
+ }
+
+ updateUI();
+
+ // Auto-play if we were playing? Let's stay stopped to be safe/explicit
+ // Or auto-play for better UX?
+ // Let's prompt user to play.
+}
+
+window.addEventListener('DOMContentLoaded', init);
+
+// Open overlay and show list of stations (used by menu/hamburger)
+function openStationsOverlay() {
+ castOverlay.classList.remove('hidden');
+ castOverlay.setAttribute('aria-hidden', 'false');
+ deviceListEl.innerHTML = 'Loading...
Preparing stations
';
+
+ // If stations not loaded yet, show message
+ if (!stations || stations.length === 0) {
+ deviceListEl.innerHTML = 'No stations found
Check your stations.json
';
+ return;
+ }
+
+ deviceListEl.innerHTML = '';
+
+ stations.forEach((s, idx) => {
+ const li = document.createElement('li');
+ li.className = 'device' + (currentIndex === idx ? ' selected' : '');
+ const subtitle = (s.raw && s.raw.www) ? s.raw.www : (s.id || '');
+ li.innerHTML = `${s.name}
${subtitle}
`;
+ li.onclick = async () => {
+ // Always switch to local playback when selecting from stations menu
+ currentMode = 'local';
+ currentCastDevice = null;
+ castBtn.style.color = 'var(--text-main)';
+
+ // Select and play
+ currentIndex = idx;
+ loadStation(currentIndex);
+ closeCastOverlay();
+ try {
+ await play();
+ } catch (e) {
+ console.error('Failed to play station from menu', e);
+ }
+ };
+ deviceListEl.appendChild(li);
+ });
+}
diff --git a/android/app/src/main/assets/stations.json b/android/app/src/main/assets/stations.json
new file mode 100644
index 0000000..51086d7
--- /dev/null
+++ b/android/app/src/main/assets/stations.json
@@ -0,0 +1,1342 @@
+[
+ {
+ "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": "https://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://www.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,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/radio1rock/json",
+ "epg": "",
+ "defaultText": "www.rockmaribor.si",
+ "www": "https://rockmaribor.si/",
+ "mountPoints": [
+ "RockMB"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://rockmaribor.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RockMaribor.si"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UC99aNwZXokG6nnJLfSn5DSw"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/radiocelje"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiocelje/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/rockmaribor?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61116",
+ "rpUid": "705107",
+ "dabUser": "celje",
+ "dabPass": "dunk7815g",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/celje/RadioCelje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Kranj",
+ "title": "Radio Kranj",
+ "slogan": "",
+ "logo": "https://data.radio.si/api/radiostations/logo/kranj.svg",
+ "liveAudio": "http://live.radio.si/Kranj",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/kranj/json",
+ "epg": "http://spored.radio.si/api/now/kranj",
+ "defaultText": "www.radio-kranj.si",
+ "www": "https://radio-kranj.si/",
+ "mountPoints": [
+ "Kranj"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+38651303505"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radio-kranj.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UCe_Ze0SEHCSLLNUbWM0aBgA"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/pages/Radio-Kranj/1760816170864847"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiokranj/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiokranj?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61102",
+ "rpUid": "705104",
+ "dabUser": "kranj",
+ "dabPass": "ui8z3Ezzosyxw",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/kranj/320x240.png",
+ "small": false
+ },
+ {
+ "id": "Celje",
+ "title": "Radio Celje",
+ "slogan": "Vedno z menoj",
+ "logo": "https://data.radio.si/api/radiostations/logo/celje.svg",
+ "liveAudio": "http://live.radio.si/Celje",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/celje/json",
+ "epg": "",
+ "defaultText": "www.radiocelje.si",
+ "www": "https://www.radiocelje.si/",
+ "mountPoints": [
+ "Celje"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+386034225100"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radiocelje.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UC99aNwZXokG6nnJLfSn5DSw"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/radiocelje"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiocelje/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "705117",
+ "dabUser": "celje",
+ "dabPass": "dunk7815g",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/celje/RadioCelje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Triglav",
+ "title": "Radio Triglav",
+ "slogan": "Radio za radovedne",
+ "logo": "https://data.radio.si/api/radiostations/logo/triglav.svg",
+ "liveAudio": "http://live.radio.si/Triglav",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/triglav/json",
+ "epg": "http://spored.radio.si/api/now/triglav",
+ "defaultText": "www.radiotriglav.si",
+ "www": "https://radiotriglav.si/",
+ "mountPoints": [
+ "Triglav"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+38651654064"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radiotriglav.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RadioTriglav"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiotriglav/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiotriglav?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=38020",
+ "rpUid": "705106",
+ "dabUser": "triglav",
+ "dabPass": "ogFLUKodMUCB5",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/triglav/320x240.png",
+ "small": false
+ },
+ {
+ "id": "Velenje",
+ "title": "Radio Velenje",
+ "slogan": "Ker smo radi na kamot",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/velenje.svg",
+ "liveAudio": "http://live.radio.si/Velenje",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/velenje/json",
+ "epg": "",
+ "defaultText": "www.veseljak.si",
+ "www": "https://veseljak.si/",
+ "mountPoints": [
+ "Velenje"
+ ],
+ "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": "705118",
+ "dabUser": "velenje",
+ "dabPass": "e9mopbu11",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/velenje/RadioVelenje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "AktualK",
+ "title": "Radio Aktual Kum",
+ "slogan": "Narejen za vaša ušesa",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/aktualk.svg",
+ "liveAudio": "http://live.radio.si/AktualK",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/aktualk/json",
+ "epg": "",
+ "defaultText": "",
+ "www": null,
+ "mountPoints": [
+ "AktualK"
+ ],
+ "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": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "AktualRomantika",
+ "title": "Radio Aktual - Romantika",
+ "slogan": "Kot nezna dlan, ki boza te",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/aktualromantika.svg",
+ "liveAudio": "http://live.radio.si/AktualRomantika",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/aktualromantika/json",
+ "epg": "",
+ "defaultText": "",
+ "www": null,
+ "mountPoints": [
+ "AktualRomantika"
+ ],
+ "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": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "705174",
+ "dabUser": "romantika",
+ "dabPass": "Z75biJ5t7CpK",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/aktualromantika/RadioAktualRomnatika_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Stop",
+ "title": "Stop",
+ "slogan": "Revija Stop: Več kot pol stoletja ob vaši strani!",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/stop.svg",
+ "liveAudio": "http://live.radio.si/Stop",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/stop/json",
+ "epg": "",
+ "defaultText": "www.revijastop.si",
+ "www": "https://revijastop.si/",
+ "mountPoints": [
+ "Stop"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://revijastop.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://totiradio.si/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Radio1SLO",
+ "title": "Radio 1 slovenski hiti",
+ "slogan": "Sama dobra slovenska glasba",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/radio1slo.svg",
+ "liveAudio": "http://live.radio.si/Radio1SLO",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/radio1slo/json",
+ "epg": "",
+ "defaultText": "www.radio1.si",
+ "www": "https://www.radio1.si",
+ "mountPoints": [
+ "Radio1SLO"
+ ],
+ "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://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": "",
+ "rpUid": "705110",
+ "dabUser": "1slovenske",
+ "dabPass": "ionb9hkd48",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/radio1slo/320x240.png",
+ "small": false
+ },
+ {
+ "id": "RockCE",
+ "title": "Rock Celje",
+ "slogan": "100% Rock",
+ "logo": "https://data.radio.si/api/radiostations/logo/rockce.svg",
+ "liveAudio": "http://live.radio.si/RockCE",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/rockce/json",
+ "epg": null,
+ "defaultText": "www.rock-celje.si",
+ "www": "https://rock-celje.si/",
+ "mountPoints": [
+ "RockCE"
+ ],
+ "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.rock-celje.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RockCelje"
+ },
+ {
+ "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": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Radio1Bozicne",
+ "title": "ROŠKARJEVE BOŽIČNE",
+ "slogan": "100% Božične",
+ "logo": "https://data.radio.si/api/radiostations/logo/radio1bozicne.svg",
+ "liveAudio": "http://live.radio1.si/Radio1Bozicne",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/radio1bozicne/json",
+ "epg": null,
+ "defaultText": "www.radio1.si",
+ "www": "https://radio1.si/",
+ "mountPoints": [
+ "Radio1Bozicne"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://radio1.si/"
+ },
+ {
+ "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": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "TotiBozicni",
+ "title": "TOTI BOŽIČNI RADIO",
+ "slogan": "Tvoje mesto, tvoj radio",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/totibozicne.svg",
+ "liveAudio": "http://live.radio.si/TotiBozicne",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/totibozicni/json",
+ "epg": null,
+ "defaultText": "www.totiradio.si",
+ "www": "https://totiradio.si/",
+ "mountPoints": [
+ "TotiBozicni"
+ ],
+ "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/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Hit",
+ "title": "Radio HIT",
+ "slogan": "Samo nostalgija",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/hit.svg",
+ "liveAudio": "http://live.radio.si/Hit",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/hit/json",
+ "epg": "http://spored.radio.si/api/now/hit",
+ "defaultText": "www.radiohit.si",
+ "www": "https://radiohit.si/",
+ "mountPoints": [
+ "Hit"
+ ],
+ "social": [],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiohit?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61120",
+ "rpUid": "705141",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": "http://media.radio.si/logo/dns/hit/320x240.png",
+ "small": false
+ }
+]
\ No newline at end of file
diff --git a/android/app/src/main/assets/styles.css b/android/app/src/main/assets/styles.css
new file mode 100644
index 0000000..37e8c62
--- /dev/null
+++ b/android/app/src/main/assets/styles.css
@@ -0,0 +1,606 @@
+: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);
+ 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);
+}
+
+/* Header */
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ -webkit-app-region: drag; /* Draggable area */
+}
+
+.header-info {
+ text-align: center;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.app-title {
+ font-weight: 600;
+ font-size: 1rem;
+ color: var(--text-main);
+}
+
+.status-indicator {
+ font-size: 0.8rem;
+ color: var(--success);
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.status-dot {
+ width: 6px;
+ height: 6px;
+ background-color: var(--success);
+ border-radius: 50%;
+ box-shadow: 0 0 8px var(--success);
+}
+
+.icon-btn {
+ background: none;
+ border: none;
+ color: var(--text-main);
+ padding: 8px;
+ cursor: pointer;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ -webkit-app-region: no-drag; /* Buttons clickable */
+}
+
+.icon-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.header-buttons {
+ display: flex;
+ gap: 4px;
+ 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.1), rgba(255,255,255,0));
+ box-shadow: 5px 5px 15px rgba(0,0,0,0.1), inset 1px 1px 2px rgba(255,255,255,0.3);
+}
+
+.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);
+}
+
+.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;
+}
+
+/* Track Info */
+.track-info {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.track-info h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+.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;
+}
+
+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;
+}
+
+/* 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;
+}
diff --git a/android/app/src/main/jniLibs/arm64-v8a/libradio_tauri_lib.so b/android/app/src/main/jniLibs/arm64-v8a/libradio_tauri_lib.so
new file mode 100644
index 0000000..c5f1aea
Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libradio_tauri_lib.so differ
diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libradio_tauri_lib.so b/android/app/src/main/jniLibs/armeabi-v7a/libradio_tauri_lib.so
new file mode 100644
index 0000000..3e48392
Binary files /dev/null and b/android/app/src/main/jniLibs/armeabi-v7a/libradio_tauri_lib.so differ
diff --git a/scripts/build-android.ps1 b/scripts/build-android.ps1
new file mode 100644
index 0000000..0d32bef
--- /dev/null
+++ b/scripts/build-android.ps1
@@ -0,0 +1,206 @@
+<#
+Build helper for Android (Windows PowerShell)
+
+What it does:
+- Checks for required commands (`npm`, `rustup`, `cargo`, `cargo-ndk`)
+- Builds frontend (runs `npm run build` if `dist`/`build` not present)
+- Copies frontend files from `dist` or `src` into `android/app/src/main/assets`
+- Builds Rust native libs using `cargo-ndk` (if available) for `aarch64` and `armv7`
+- Copies produced `.so` files into `android/app/src/main/jniLibs/*`
+
+Note: This script prepares the Android project. To produce the APK, open `android/` in Android Studio and run Build -> Assemble, or run `gradlew assembleDebug` locally.
+#>
+
+Set-StrictMode -Version Latest
+
+function Check-Command($name) {
+ $which = Get-Command $name -ErrorAction SilentlyContinue
+ return $which -ne $null
+}
+
+Write-Output "Starting Android prep script..."
+
+if (-not (Check-Command npm)) { Write-Warning "npm not found in PATH. Install Node.js to build frontend." }
+if (-not (Check-Command rustup)) { Write-Warning "rustup not found in PATH. Install Rust toolchain." }
+if (-not (Check-Command cargo)) { Write-Warning "cargo not found in PATH." }
+
+$cargoNdkAvailable = Check-Command cargo-ndk
+if (-not $cargoNdkAvailable) { Write-Warning "cargo-ndk not found. Native libs will not be built. Install via 'cargo install cargo-ndk'" }
+
+# Determine repository root (parent of the scripts folder)
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$root = Split-Path -Parent $scriptDir
+Push-Location $root
+
+# Prefer Tauri-generated Android Studio project (tauri android init)
+$androidRoot = Join-Path $root 'src-tauri\gen\android'
+if (-not (Test-Path $androidRoot)) {
+ # Legacy fallback (non-Tauri project)
+ $androidRoot = Join-Path $root 'android'
+}
+
+function Escape-LocalPropertiesPath([string]$p) {
+ # local.properties expects ':' escaped and backslashes doubled on Windows.
+ # Use plain string replacements to avoid regex escaping pitfalls.
+ return ($p.Replace('\', '\\').Replace(':', '\:'))
+}
+
+# Ensure Android SDK/NDK locations are set for Gradle (local.properties)
+$sdkRoot = $env:ANDROID_SDK_ROOT
+if (-not $sdkRoot) { $sdkRoot = $env:ANDROID_HOME }
+if (-not $sdkRoot) { $sdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
+
+$ndkRoot = $env:ANDROID_NDK_ROOT
+if (-not $ndkRoot) { $ndkRoot = $env:ANDROID_NDK_HOME }
+if (-not $ndkRoot -and (Test-Path (Join-Path $sdkRoot 'ndk'))) {
+ $ndkVersions = Get-ChildItem -Path (Join-Path $sdkRoot 'ndk') -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending
+ if ($ndkVersions -and (@($ndkVersions)).Count -gt 0) { $ndkRoot = @($ndkVersions)[0].FullName }
+}
+
+if (Test-Path $androidRoot) {
+ $localPropsPath = Join-Path $androidRoot 'local.properties'
+ $lines = @()
+ if ($sdkRoot) { $lines += "sdk.dir=$(Escape-LocalPropertiesPath $sdkRoot)" }
+ if ($ndkRoot) { $lines += "ndk.dir=$(Escape-LocalPropertiesPath $ndkRoot)" }
+ if ($lines.Count -gt 0) {
+ Set-Content -Path $localPropsPath -Value ($lines -join "`n") -Encoding ASCII
+ Write-Output "Wrote Android SDK/NDK config to: $localPropsPath"
+ }
+}
+
+# Build frontend (optional)
+Write-Output "Preparing frontend files..."
+$distDirs = @('dist','build')
+$foundDist = $null
+foreach ($d in $distDirs) {
+ if (Test-Path (Join-Path $root $d)) { $foundDist = $d; break }
+}
+
+if (-not $foundDist) {
+ # IMPORTANT: `npm run build` in this repo runs `tauri build`, which is a desktop bundling step.
+ # For Android prep we only need web assets, so we fall back to copying `src/` as assets.
+ Write-Warning "No dist/build output found — copying `src/` as assets (skipping `npm run build` to avoid desktop bundling)."
+}
+
+$assetsDst = Join-Path $androidRoot 'app\src\main\assets'
+if (-not (Test-Path $assetsDst)) { New-Item -ItemType Directory -Path $assetsDst -Force | Out-Null }
+
+if ($foundDist) {
+ Write-Output "Copying frontend from '$foundDist' to Android assets..."
+ robocopy (Join-Path $root $foundDist) $assetsDst /MIR | Out-Null
+} else {
+ Write-Output "Copying raw 'src' to Android assets..."
+ robocopy (Join-Path $root 'src') $assetsDst /MIR | Out-Null
+}
+
+# Build native libs if cargo-ndk available
+if ($cargoNdkAvailable) {
+ Write-Output "Building Rust native libs via cargo-ndk from project root: $root"
+ try {
+ # Build from the Rust crate directory `src-tauri`
+ $crateDir = Join-Path $root 'src-tauri'
+ if (-not (Test-Path (Join-Path $crateDir 'Cargo.toml'))) {
+ Write-Warning "Cargo.toml not found in src-tauri; skipping native build."
+ } else {
+ # Prefer Ninja generator for CMake if available (avoids Visual Studio generator issues)
+ # Restore env vars at the end so we don't pollute the current PowerShell session.
+ $oldCmakeGenerator = $env:CMAKE_GENERATOR
+ $oldCmakeMakeProgram = $env:CMAKE_MAKE_PROGRAM
+ $ninjaCmd = Get-Command ninja -ErrorAction SilentlyContinue
+ if ($ninjaCmd) {
+ Write-Output "Ninja detected at $($ninjaCmd.Source); setting CMake generator to Ninja."
+ $env:CMAKE_GENERATOR = 'Ninja'
+ $env:CMAKE_MAKE_PROGRAM = $ninjaCmd.Source
+ } else {
+ Write-Warning "Ninja not found in PATH. Installing Ninja or adding it to PATH is strongly recommended to avoid Visual Studio CMake generator on Windows."
+ }
+
+ # Attempt to locate Android NDK if environment variables are not set
+ if (-not $env:ANDROID_NDK_ROOT -and -not $env:ANDROID_NDK_HOME) {
+ $candidates = @()
+ if ($env:ANDROID_SDK_ROOT) { $candidates += Join-Path $env:ANDROID_SDK_ROOT 'ndk' }
+ if ($env:ANDROID_HOME) { $candidates += Join-Path $env:ANDROID_HOME 'ndk' }
+ $candidates += Join-Path $env:LOCALAPPDATA 'Android\sdk\ndk'
+ $candidates += Join-Path $env:USERPROFILE 'AppData\Local\Android\sdk\ndk'
+ $candidates += 'C:\Program Files (x86)\Android\AndroidNDK'
+
+ foreach ($cand in $candidates) {
+ if (Test-Path $cand) {
+ $versions = Get-ChildItem -Path $cand -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending
+ if ($versions -and (@($versions)).Count -gt 0) {
+ $ndkPath = @($versions)[0].FullName
+ Write-Output "Detected Android NDK at: $ndkPath"
+ $env:ANDROID_NDK_ROOT = $ndkPath
+ $env:ANDROID_NDK = $ndkPath
+ break
+ }
+ }
+ }
+ if (-not $env:ANDROID_NDK_ROOT) { Write-Warning "ANDROID_NDK_ROOT/ANDROID_NDK not set and no NDK found in common locations. Set ANDROID_NDK_ROOT to your NDK path." }
+ } else {
+ Write-Output "Using existing ANDROID_NDK_ROOT: $($env:ANDROID_NDK_ROOT)"
+ if (-not $env:ANDROID_NDK) { $env:ANDROID_NDK = $env:ANDROID_NDK_ROOT }
+ }
+
+ # Ensure expected external binary placeholders exist so Tauri bundling doesn't fail
+ $binariesDir = Join-Path $crateDir 'binaries'
+ if (-not (Test-Path $binariesDir)) { New-Item -ItemType Directory -Path $binariesDir -Force | Out-Null }
+ $placeholder1 = Join-Path $binariesDir 'RadioPlayer-aarch64-linux-android'
+ $placeholder2 = Join-Path $binariesDir 'RadioPlayer-armv7-linux-androideabi'
+ if (-not (Test-Path $placeholder1)) { New-Item -ItemType File -Path $placeholder1 -Force | Out-Null; Write-Output "Created placeholder: $placeholder1" }
+ if (-not (Test-Path $placeholder2)) { New-Item -ItemType File -Path $placeholder2 -Force | Out-Null; Write-Output "Created placeholder: $placeholder2" }
+
+ # If a previous build used a different CMake generator (e.g., Visual Studio), aws-lc-sys can fail with
+ # "Does not match the generator used previously". Clean only the aws-lc-sys CMake build dirs.
+ $awsLcBuildDirs = Get-ChildItem -Path (Join-Path $crateDir 'target') -Recurse -Directory -ErrorAction SilentlyContinue |
+ Where-Object { $_.Name -like 'aws-lc-sys-*' }
+ foreach ($d in @($awsLcBuildDirs)) {
+ $cmakeBuildDir = Join-Path $d.FullName 'out\build'
+ $cmakeCache = Join-Path $cmakeBuildDir 'CMakeCache.txt'
+ if (Test-Path $cmakeCache) {
+ Write-Output "Cleaning stale CMake cache for aws-lc-sys: $cmakeBuildDir"
+ Remove-Item -Path $cmakeBuildDir -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ Push-Location $crateDir
+ try {
+ # Use API 24 to ensure libc symbols like getifaddrs/freeifaddrs are available.
+ # Build only the library to avoid linking the desktop binary for Android.
+ Write-Output "Running: cargo ndk -t arm64-v8a -t armeabi-v7a -P 24 build --release --lib (in $crateDir)"
+ cargo ndk -t arm64-v8a -t armeabi-v7a -P 24 build --release --lib
+ } finally {
+ Pop-Location
+ if ($null -eq $oldCmakeGenerator) { Remove-Item Env:\CMAKE_GENERATOR -ErrorAction SilentlyContinue } else { $env:CMAKE_GENERATOR = $oldCmakeGenerator }
+ if ($null -eq $oldCmakeMakeProgram) { Remove-Item Env:\CMAKE_MAKE_PROGRAM -ErrorAction SilentlyContinue } else { $env:CMAKE_MAKE_PROGRAM = $oldCmakeMakeProgram }
+ }
+
+ # Search for produced .so files under src-tauri/target
+ $soFiles = Get-ChildItem -Path (Join-Path $crateDir 'target') -Recurse -Filter "*.so" -ErrorAction SilentlyContinue
+ if (-not $soFiles) {
+ Write-Warning "No .so files found after build. Check cargo-ndk output above for errors."
+ } else {
+ foreach ($f in @($soFiles)) {
+ $full = $f.FullName
+ if ($full -match 'aarch64|aarch64-linux-android|arm64-v8a') { $abi = 'arm64-v8a' }
+ elseif ($full -match 'armv7|armv7-linux-androideabi|armeabi-v7a') { $abi = 'armeabi-v7a' }
+ else { continue }
+
+ $dst = Join-Path $androidRoot "app\src\main\jniLibs\$abi"
+ if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst -Force | Out-Null }
+ Copy-Item $full -Destination $dst -Force
+ Write-Output "Copied $($f.Name) -> $dst"
+ }
+ }
+ }
+ } catch {
+ Write-Warning "cargo-ndk build failed. Exception: $($_.Exception.Message)"
+ if ($_.ScriptStackTrace) { Write-Output $_.ScriptStackTrace }
+ }
+} else {
+ Write-Warning "Skipping native lib build (cargo-ndk missing)."
+}
+
+Write-Output "Android prep complete. Open '$androidRoot' in Android Studio and build the APK (or run './gradlew assembleDebug' in that folder)."
+
+Pop-Location
diff --git a/scripts/build-android.sh b/scripts/build-android.sh
new file mode 100644
index 0000000..769c9e9
--- /dev/null
+++ b/scripts/build-android.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Cross-platform helper for Unix-like shells
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+echo "Preparing Android assets and native libs..."
+
+if command -v npm >/dev/null 2>&1; then
+ echo "Running npm install & build"
+ npm install
+ npm run build || true
+fi
+
+DIST_DIR="dist"
+if [ ! -d "$DIST_DIR" ]; then DIST_DIR="build"; fi
+if [ -d "$DIST_DIR" ]; then
+ echo "Copying $DIST_DIR -> android/app/src/main/assets"
+ mkdir -p android/app/src/main/assets
+ rsync -a --delete "$DIST_DIR/" android/app/src/main/assets/
+else
+ echo "No dist/build found, copying src/ -> android assets"
+ mkdir -p android/app/src/main/assets
+ rsync -a --delete src/ android/app/src/main/assets/
+fi
+
+if command -v cargo-ndk >/dev/null 2>&1; then
+ echo "Building native libs with cargo-ndk"
+ cargo-ndk -t aarch64 -t armv7 build --release || true
+ # copy so files
+ find target -type f -name "*.so" | while read -r f; do
+ if [[ "$f" =~ aarch64|aarch64-linux-android ]]; then abi=arm64-v8a; fi
+ if [[ "$f" =~ armv7|armv7-linux-androideabi ]]; then abi=armeabi-v7a; fi
+ if [ -n "${abi-}" ]; then
+ mkdir -p android/app/src/main/jniLibs/$abi
+ cp "$f" android/app/src/main/jniLibs/$abi/
+ echo "Copied $f -> android/app/src/main/jniLibs/$abi/"
+ fi
+ done
+else
+ echo "cargo-ndk not found; skipping native lib build"
+fi
+
+echo "Prepared Android project. Open android/ in Android Studio to build the APK (or run ./gradlew assembleDebug)."
diff --git a/src-tauri/binaries/RadioPlayer-aarch64-linux-android b/src-tauri/binaries/RadioPlayer-aarch64-linux-android
new file mode 100644
index 0000000..e69de29
diff --git a/src-tauri/binaries/RadioPlayer-armv7-linux-androideabi b/src-tauri/binaries/RadioPlayer-armv7-linux-androideabi
new file mode 100644
index 0000000..e69de29
diff --git a/src-tauri/gen/android/.editorconfig b/src-tauri/gen/android/.editorconfig
new file mode 100644
index 0000000..ebe51d3
--- /dev/null
+++ b/src-tauri/gen/android/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = false
\ No newline at end of file
diff --git a/src-tauri/gen/android/.gitignore b/src-tauri/gen/android/.gitignore
new file mode 100644
index 0000000..b248203
--- /dev/null
+++ b/src-tauri/gen/android/.gitignore
@@ -0,0 +1,19 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+key.properties
+
+/.tauri
+/tauri.settings.gradle
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/.gitignore b/src-tauri/gen/android/app/.gitignore
new file mode 100644
index 0000000..dc372eb
--- /dev/null
+++ b/src-tauri/gen/android/app/.gitignore
@@ -0,0 +1,6 @@
+/src/main/java/si/klevze/radioPlayer/generated
+/src/main/jniLibs/**/*.so
+/src/main/assets/tauri.conf.json
+/tauri.build.gradle.kts
+/proguard-tauri.pro
+/tauri.properties
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts
new file mode 100644
index 0000000..7083e3f
--- /dev/null
+++ b/src-tauri/gen/android/app/build.gradle.kts
@@ -0,0 +1,64 @@
+import java.util.Properties
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+val tauriProperties = Properties().apply {
+ val propFile = file("tauri.properties")
+ if (propFile.exists()) {
+ propFile.inputStream().use { load(it) }
+ }
+}
+
+android {
+ compileSdk = 36
+ namespace = "si.klevze.radioPlayer"
+ defaultConfig {
+ manifestPlaceholders["usesCleartextTraffic"] = "false"
+ applicationId = "si.klevze.radioPlayer"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
+ versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
+ }
+ buildTypes {
+ getByName("debug") {
+ manifestPlaceholders["usesCleartextTraffic"] = "true"
+ isDebuggable = true
+ isJniDebuggable = true
+ isMinifyEnabled = false
+ packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
+ jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
+ jniLibs.keepDebugSymbols.add("*/x86/*.so")
+ jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
+ }
+ }
+ getByName("release") {
+ isMinifyEnabled = true
+ proguardFiles(
+ *fileTree(".") { include("**/*.pro") }
+ .plus(getDefaultProguardFile("proguard-android-optimize.txt"))
+ .toList().toTypedArray()
+ )
+ }
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+dependencies {
+ implementation("androidx.constraintlayout:constraintlayout:2.2.1")
+ implementation("androidx.webkit:webkit:1.14.0")
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("androidx.activity:activity-ktx:1.10.1")
+ implementation("com.google.android.material:material:1.12.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.4")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
+}
diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/src-tauri/gen/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5a5c9a8
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/appIcon.png b/src-tauri/gen/android/app/src/main/assets/assets/appIcon.png
new file mode 100644
index 0000000..d789348
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/appIcon.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io.zip b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io.zip
new file mode 100644
index 0000000..15adffb
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io.zip differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png
new file mode 100644
index 0000000..4757781
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-192x192.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png
new file mode 100644
index 0000000..8f1299a
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/android-chrome-512x512.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/app-icon.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/app-icon.png
new file mode 100644
index 0000000..8f1299a
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/app-icon.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png
new file mode 100644
index 0000000..f29ebcf
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/apple-touch-icon.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png
new file mode 100644
index 0000000..4eb99da
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-16x16.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png
new file mode 100644
index 0000000..73c330b
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/favicon-32x32.png differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/icon.ico b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/icon.ico
new file mode 100644
index 0000000..586e969
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/icon.ico differ
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/site.webmanifest b/src-tauri/gen/android/app/src/main/assets/assets/favicon_io/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/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"}
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/javascript.svg b/src-tauri/gen/android/app/src/main/assets/assets/javascript.svg
new file mode 100644
index 0000000..f9abb2b
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/assets/javascript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/assets/assets/tauri.svg b/src-tauri/gen/android/app/src/main/assets/assets/tauri.svg
new file mode 100644
index 0000000..0c0e6aa
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/assets/tauri.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src-tauri/gen/android/app/src/main/assets/index.html b/src-tauri/gen/android/app/src/main/assets/index.html
new file mode 100644
index 0000000..3ea45e7
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/index.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+ Radio1 Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+ Radio 1 MB
+ Live Stream
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose
+
+
+
+
+ Scanning...
+ Searching for speakers
+
+
+
+
Cancel
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/assets/main.js b/src-tauri/gen/android/app/src/main/assets/main.js
new file mode 100644
index 0000000..50c6b53
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/main.js
@@ -0,0 +1,355 @@
+const { invoke } = window.__TAURI__.core;
+const { getCurrentWindow } = window.__TAURI__.window;
+
+// State
+let stations = [];
+let currentIndex = 0;
+let isPlaying = false;
+let currentMode = 'local'; // 'local' | 'cast'
+let currentCastDevice = null;
+const audio = new Audio();
+
+// UI Elements
+const stationNameEl = document.getElementById('station-name');
+const stationSubtitleEl = document.getElementById('station-subtitle');
+const statusTextEl = document.getElementById('status-text');
+const statusDotEl = document.querySelector('.status-dot');
+const playBtn = document.getElementById('play-btn');
+const iconPlay = document.getElementById('icon-play');
+const iconStop = document.getElementById('icon-stop');
+const prevBtn = document.getElementById('prev-btn');
+const nextBtn = document.getElementById('next-btn');
+const volumeSlider = document.getElementById('volume-slider');
+const volumeValue = document.getElementById('volume-value');
+const castBtn = document.getElementById('cast-toggle-btn');
+const castOverlay = document.getElementById('cast-overlay');
+const closeOverlayBtn = document.getElementById('close-overlay');
+const deviceListEl = document.getElementById('device-list');
+const logoTextEl = document.querySelector('.station-logo-text');
+const logoImgEl = document.getElementById('station-logo-img');
+
+// Init
+async function init() {
+ await loadStations();
+ setupEventListeners();
+ updateUI();
+}
+
+async function loadStations() {
+ try {
+ 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`.
+ stations = raw
+ .map((s) => {
+ // If already in the old format, keep as-is
+ if (s.name && s.url) return s;
+
+ const name = s.title || s.id || s.name || 'Unknown';
+ // Prefer liveAudio, fall back to liveVideo or any common fields
+ const url = s.liveAudio || s.liveVideo || s.liveStream || s.url || '';
+
+ return {
+ id: s.id || name,
+ name,
+ url,
+ logo: s.logo || s.poster || '',
+ enabled: typeof s.enabled === 'boolean' ? s.enabled : true,
+ raw: s,
+ };
+ })
+ // Filter out disabled stations and those without a stream URL
+ .filter((s) => s.enabled !== false && s.url && s.url.length > 0);
+
+ if (stations.length > 0) {
+ currentIndex = 0;
+ loadStation(currentIndex);
+ }
+ } catch (e) {
+ console.error('Failed to load stations', e);
+ statusTextEl.textContent = 'Error loading stations';
+ }
+}
+
+function setupEventListeners() {
+ playBtn.addEventListener('click', togglePlay);
+ prevBtn.addEventListener('click', playPrev);
+ nextBtn.addEventListener('click', playNext);
+
+ volumeSlider.addEventListener('input', handleVolumeInput);
+
+ castBtn.addEventListener('click', openCastOverlay);
+ closeOverlayBtn.addEventListener('click', closeCastOverlay);
+
+ // Close overlay on background click
+ castOverlay.addEventListener('click', (e) => {
+ if (e.target === castOverlay) closeCastOverlay();
+ });
+
+ // Close button
+ document.getElementById('close-btn').addEventListener('click', async () => {
+ const appWindow = getCurrentWindow();
+ await appWindow.close();
+ });
+
+ // Menu button - explicit functionality or placeholder?
+ // For now just log or maybe show about
+ document.getElementById('menu-btn').addEventListener('click', () => {
+ openStationsOverlay();
+ });
+
+ // Hotkeys?
+}
+
+function loadStation(index) {
+ if (index < 0 || index >= stations.length) return;
+ const station = stations[index];
+
+ stationNameEl.textContent = station.name;
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
+
+ // Update Logo Text (First letter or number)
+ // Simple heuristic: if name has a number, use it, else first letter
+ // If station has a logo URL, show the image; otherwise show the text fallback
+ if (station.logo && station.logo.length > 0) {
+ logoImgEl.src = station.logo;
+ logoImgEl.classList.remove('hidden');
+ logoTextEl.classList.add('hidden');
+ } else {
+ // Fallback to single-letter/logo text
+ logoImgEl.src = '';
+ logoImgEl.classList.add('hidden');
+ const numberMatch = station.name.match(/\d+/);
+ if (numberMatch) {
+ logoTextEl.textContent = numberMatch[0];
+ } else {
+ logoTextEl.textContent = station.name.charAt(0).toUpperCase();
+ }
+ logoTextEl.classList.remove('hidden');
+ }
+}
+
+async function togglePlay() {
+ if (isPlaying) {
+ await stop();
+ } else {
+ await play();
+ }
+}
+
+async function play() {
+ const station = stations[currentIndex];
+ if (!station) return;
+
+ statusTextEl.textContent = 'Buffering...';
+ statusDotEl.style.backgroundColor = 'var(--text-muted)'; // Grey/Yellow while loading
+
+ if (currentMode === 'local') {
+ audio.src = station.url;
+ audio.volume = volumeSlider.value / 100;
+ try {
+ await audio.play();
+ isPlaying = true;
+ updateUI();
+ } catch (e) {
+ console.error('Playback failed', e);
+ statusTextEl.textContent = 'Error';
+ }
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ // Cast logic
+ try {
+ await invoke('cast_play', { deviceName: currentCastDevice, url: station.url });
+ isPlaying = true;
+ // Sync volume
+ const vol = volumeSlider.value / 100;
+ invoke('cast_set_volume', { deviceName: currentCastDevice, volume: vol });
+ updateUI();
+ } catch (e) {
+ console.error('Cast failed', e);
+ statusTextEl.textContent = 'Cast Error';
+ currentMode = 'local'; // Fallback
+ updateUI();
+ }
+ }
+}
+
+async function stop() {
+ if (currentMode === 'local') {
+ audio.pause();
+ audio.src = '';
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ try {
+ await invoke('cast_stop', { deviceName: currentCastDevice });
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ isPlaying = false;
+ updateUI();
+}
+
+async function playNext() {
+ if (stations.length === 0) return;
+
+ // If playing, stop first? Or seamless?
+ // For radio, seamless switch requires stop then play new URL
+ const wasPlaying = isPlaying;
+
+ if (wasPlaying) await stop();
+
+ currentIndex = (currentIndex + 1) % stations.length;
+ loadStation(currentIndex);
+
+ if (wasPlaying) await play();
+}
+
+async function playPrev() {
+ if (stations.length === 0) return;
+
+ const wasPlaying = isPlaying;
+
+ if (wasPlaying) await stop();
+
+ currentIndex = (currentIndex - 1 + stations.length) % stations.length;
+ loadStation(currentIndex);
+
+ if (wasPlaying) await play();
+}
+
+function updateUI() {
+ // Play/Stop Button
+ if (isPlaying) {
+ iconPlay.classList.add('hidden');
+ iconStop.classList.remove('hidden');
+ playBtn.classList.add('playing'); // Add pulsing ring animation
+ statusTextEl.textContent = 'Playing';
+ statusDotEl.style.backgroundColor = 'var(--success)';
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
+ } else {
+ iconPlay.classList.remove('hidden');
+ iconStop.classList.add('hidden');
+ playBtn.classList.remove('playing'); // Remove pulsing ring
+ statusTextEl.textContent = 'Ready';
+ statusDotEl.style.backgroundColor = 'var(--text-muted)';
+ stationSubtitleEl.textContent = currentMode === 'cast' ? `Connected to ${currentCastDevice}` : 'Live Stream';
+ }
+}
+
+function handleVolumeInput() {
+ const val = volumeSlider.value;
+ volumeValue.textContent = `${val}%`;
+ const decimals = val / 100;
+
+ if (currentMode === 'local') {
+ audio.volume = decimals;
+ } else if (currentMode === 'cast' && currentCastDevice) {
+ invoke('cast_set_volume', { deviceName: currentCastDevice, volume: decimals });
+ }
+}
+
+// Cast Logic
+async function openCastOverlay() {
+ castOverlay.classList.remove('hidden');
+ castOverlay.setAttribute('aria-hidden', 'false');
+ deviceListEl.innerHTML = 'Scanning...
Searching for speakers
';
+
+ try {
+ const devices = await invoke('list_cast_devices');
+ deviceListEl.innerHTML = '';
+
+ // Add "This Computer" option
+ const localLi = document.createElement('li');
+ localLi.className = 'device' + (currentMode === 'local' ? ' selected' : '');
+ localLi.innerHTML = 'This Computer
Local Playback
';
+ localLi.onclick = () => selectCastDevice(null);
+ deviceListEl.appendChild(localLi);
+
+ if (devices.length > 0) {
+ devices.forEach(d => {
+ const li = document.createElement('li');
+ li.className = 'device' + (currentMode === 'cast' && currentCastDevice === d ? ' selected' : '');
+ li.innerHTML = `${d}
Google Cast Speaker
`;
+ li.onclick = () => selectCastDevice(d);
+ deviceListEl.appendChild(li);
+ });
+ }
+ } catch (e) {
+ deviceListEl.innerHTML = `Error
${e}
`;
+ }
+}
+
+function closeCastOverlay() {
+ castOverlay.classList.add('hidden');
+ castOverlay.setAttribute('aria-hidden', 'true');
+}
+
+async function selectCastDevice(deviceName) {
+ closeCastOverlay();
+
+ // If checking same device, do nothing
+ if (deviceName === currentCastDevice) return;
+
+ // If switching mode, stop current playback
+ if (isPlaying) {
+ await stop();
+ }
+
+ if (deviceName) {
+ currentMode = 'cast';
+ currentCastDevice = deviceName;
+ castBtn.style.color = 'var(--success)';
+ } else {
+ currentMode = 'local';
+ currentCastDevice = null;
+ castBtn.style.color = 'var(--text-main)';
+ }
+
+ updateUI();
+
+ // Auto-play if we were playing? Let's stay stopped to be safe/explicit
+ // Or auto-play for better UX?
+ // Let's prompt user to play.
+}
+
+window.addEventListener('DOMContentLoaded', init);
+
+// Open overlay and show list of stations (used by menu/hamburger)
+function openStationsOverlay() {
+ castOverlay.classList.remove('hidden');
+ castOverlay.setAttribute('aria-hidden', 'false');
+ deviceListEl.innerHTML = 'Loading...
Preparing stations
';
+
+ // If stations not loaded yet, show message
+ if (!stations || stations.length === 0) {
+ deviceListEl.innerHTML = 'No stations found
Check your stations.json
';
+ return;
+ }
+
+ deviceListEl.innerHTML = '';
+
+ stations.forEach((s, idx) => {
+ const li = document.createElement('li');
+ li.className = 'device' + (currentIndex === idx ? ' selected' : '');
+ const subtitle = (s.raw && s.raw.www) ? s.raw.www : (s.id || '');
+ li.innerHTML = `${s.name}
${subtitle}
`;
+ li.onclick = async () => {
+ // Always switch to local playback when selecting from stations menu
+ currentMode = 'local';
+ currentCastDevice = null;
+ castBtn.style.color = 'var(--text-main)';
+
+ // Select and play
+ currentIndex = idx;
+ loadStation(currentIndex);
+ closeCastOverlay();
+ try {
+ await play();
+ } catch (e) {
+ console.error('Failed to play station from menu', e);
+ }
+ };
+ deviceListEl.appendChild(li);
+ });
+}
diff --git a/src-tauri/gen/android/app/src/main/assets/stations.json b/src-tauri/gen/android/app/src/main/assets/stations.json
new file mode 100644
index 0000000..51086d7
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/stations.json
@@ -0,0 +1,1342 @@
+[
+ {
+ "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": "https://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://www.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,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/radio1rock/json",
+ "epg": "",
+ "defaultText": "www.rockmaribor.si",
+ "www": "https://rockmaribor.si/",
+ "mountPoints": [
+ "RockMB"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://rockmaribor.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RockMaribor.si"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UC99aNwZXokG6nnJLfSn5DSw"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/radiocelje"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiocelje/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/rockmaribor?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61116",
+ "rpUid": "705107",
+ "dabUser": "celje",
+ "dabPass": "dunk7815g",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/celje/RadioCelje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Kranj",
+ "title": "Radio Kranj",
+ "slogan": "",
+ "logo": "https://data.radio.si/api/radiostations/logo/kranj.svg",
+ "liveAudio": "http://live.radio.si/Kranj",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/kranj/json",
+ "epg": "http://spored.radio.si/api/now/kranj",
+ "defaultText": "www.radio-kranj.si",
+ "www": "https://radio-kranj.si/",
+ "mountPoints": [
+ "Kranj"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+38651303505"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radio-kranj.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UCe_Ze0SEHCSLLNUbWM0aBgA"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/pages/Radio-Kranj/1760816170864847"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiokranj/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiokranj?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61102",
+ "rpUid": "705104",
+ "dabUser": "kranj",
+ "dabPass": "ui8z3Ezzosyxw",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/kranj/320x240.png",
+ "small": false
+ },
+ {
+ "id": "Celje",
+ "title": "Radio Celje",
+ "slogan": "Vedno z menoj",
+ "logo": "https://data.radio.si/api/radiostations/logo/celje.svg",
+ "liveAudio": "http://live.radio.si/Celje",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/celje/json",
+ "epg": "",
+ "defaultText": "www.radiocelje.si",
+ "www": "https://www.radiocelje.si/",
+ "mountPoints": [
+ "Celje"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+386034225100"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radiocelje.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/channel/UC99aNwZXokG6nnJLfSn5DSw"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/radiocelje"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiocelje/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "705117",
+ "dabUser": "celje",
+ "dabPass": "dunk7815g",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/celje/RadioCelje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Triglav",
+ "title": "Radio Triglav",
+ "slogan": "Radio za radovedne",
+ "logo": "https://data.radio.si/api/radiostations/logo/triglav.svg",
+ "liveAudio": "http://live.radio.si/Triglav",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/triglav/json",
+ "epg": "http://spored.radio.si/api/now/triglav",
+ "defaultText": "www.radiotriglav.si",
+ "www": "https://radiotriglav.si/",
+ "mountPoints": [
+ "Triglav"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-telefon-01.svg",
+ "link": "tel:+38651654064"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://www.radiotriglav.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RadioTriglav"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-instagram-01.svg",
+ "link": "https://www.instagram.com/radiotriglav/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiotriglav?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=38020",
+ "rpUid": "705106",
+ "dabUser": "triglav",
+ "dabPass": "ogFLUKodMUCB5",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/triglav/320x240.png",
+ "small": false
+ },
+ {
+ "id": "Velenje",
+ "title": "Radio Velenje",
+ "slogan": "Ker smo radi na kamot",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/velenje.svg",
+ "liveAudio": "http://live.radio.si/Velenje",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/velenje/json",
+ "epg": "",
+ "defaultText": "www.veseljak.si",
+ "www": "https://veseljak.si/",
+ "mountPoints": [
+ "Velenje"
+ ],
+ "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": "705118",
+ "dabUser": "velenje",
+ "dabPass": "e9mopbu11",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/velenje/RadioVelenje_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "AktualK",
+ "title": "Radio Aktual Kum",
+ "slogan": "Narejen za vaša ušesa",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/aktualk.svg",
+ "liveAudio": "http://live.radio.si/AktualK",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/aktualk/json",
+ "epg": "",
+ "defaultText": "",
+ "www": null,
+ "mountPoints": [
+ "AktualK"
+ ],
+ "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": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "AktualRomantika",
+ "title": "Radio Aktual - Romantika",
+ "slogan": "Kot nezna dlan, ki boza te",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/aktualromantika.svg",
+ "liveAudio": "http://live.radio.si/AktualRomantika",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/aktualromantika/json",
+ "epg": "",
+ "defaultText": "",
+ "www": null,
+ "mountPoints": [
+ "AktualRomantika"
+ ],
+ "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": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "705174",
+ "dabUser": "romantika",
+ "dabPass": "Z75biJ5t7CpK",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/aktualromantika/RadioAktualRomnatika_DAB.jpg",
+ "small": false
+ },
+ {
+ "id": "Stop",
+ "title": "Stop",
+ "slogan": "Revija Stop: Več kot pol stoletja ob vaši strani!",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/stop.svg",
+ "liveAudio": "http://live.radio.si/Stop",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/stop/json",
+ "epg": "",
+ "defaultText": "www.revijastop.si",
+ "www": "https://revijastop.si/",
+ "mountPoints": [
+ "Stop"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://revijastop.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://totiradio.si/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Radio1SLO",
+ "title": "Radio 1 slovenski hiti",
+ "slogan": "Sama dobra slovenska glasba",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/radio1slo.svg",
+ "liveAudio": "http://live.radio.si/Radio1SLO",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/radio1slo/json",
+ "epg": "",
+ "defaultText": "www.radio1.si",
+ "www": "https://www.radio1.si",
+ "mountPoints": [
+ "Radio1SLO"
+ ],
+ "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://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": "",
+ "rpUid": "705110",
+ "dabUser": "1slovenske",
+ "dabPass": "ionb9hkd48",
+ "dabDefaultImg": "http://media.radio.si/logo/dns/radio1slo/320x240.png",
+ "small": false
+ },
+ {
+ "id": "RockCE",
+ "title": "Rock Celje",
+ "slogan": "100% Rock",
+ "logo": "https://data.radio.si/api/radiostations/logo/rockce.svg",
+ "liveAudio": "http://live.radio.si/RockCE",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/rockce/json",
+ "epg": null,
+ "defaultText": "www.rock-celje.si",
+ "www": "https://rock-celje.si/",
+ "mountPoints": [
+ "RockCE"
+ ],
+ "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.rock-celje.si/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-facebook-01.svg",
+ "link": "https://www.facebook.com/RockCelje"
+ },
+ {
+ "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": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Radio1Bozicne",
+ "title": "ROŠKARJEVE BOŽIČNE",
+ "slogan": "100% Božične",
+ "logo": "https://data.radio.si/api/radiostations/logo/radio1bozicne.svg",
+ "liveAudio": "http://live.radio1.si/Radio1Bozicne",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "https://data.radio.si/api/lastsongsxml/radio1bozicne/json",
+ "epg": null,
+ "defaultText": "www.radio1.si",
+ "www": "https://radio1.si/",
+ "mountPoints": [
+ "Radio1Bozicne"
+ ],
+ "social": [
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-www-01.svg",
+ "link": "https://radio1.si/"
+ },
+ {
+ "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": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "TotiBozicni",
+ "title": "TOTI BOŽIČNI RADIO",
+ "slogan": "Tvoje mesto, tvoj radio",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/totibozicne.svg",
+ "liveAudio": "http://live.radio.si/TotiBozicne",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/totibozicni/json",
+ "epg": null,
+ "defaultText": "www.totiradio.si",
+ "www": "https://totiradio.si/",
+ "mountPoints": [
+ "TotiBozicni"
+ ],
+ "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/"
+ },
+ {
+ "icon": "http://datacache.radio.si/api/radiostations/logo/ikona-youtube-01.svg",
+ "link": "https://www.youtube.com/user/raktual"
+ },
+ {
+ "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/stories/radioaktual/"
+ }
+ ],
+ "enabled": true,
+ "radioApiIO": "",
+ "rpUid": "",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": null,
+ "small": false
+ },
+ {
+ "id": "Hit",
+ "title": "Radio HIT",
+ "slogan": "Samo nostalgija",
+ "logo": "http://datacache.radio.si/api/radiostations/logo/hit.svg",
+ "liveAudio": "http://live.radio.si/Hit",
+ "liveVideo": null,
+ "poster": null,
+ "lastSongs": "http://data.radio.si/api/lastsongsxml/hit/json",
+ "epg": "http://spored.radio.si/api/now/hit",
+ "defaultText": "www.radiohit.si",
+ "www": "https://radiohit.si/",
+ "mountPoints": [
+ "Hit"
+ ],
+ "social": [],
+ "enabled": true,
+ "radioApiIO": "https://onair.radioapi.io/ingest/infonet/radiohit?format=aim&key=83B0Cq0wZH5DmLyZ13qr&cid=61120",
+ "rpUid": "705141",
+ "dabUser": null,
+ "dabPass": null,
+ "dabDefaultImg": "http://media.radio.si/logo/dns/hit/320x240.png",
+ "small": false
+ }
+]
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/assets/styles.css b/src-tauri/gen/android/app/src/main/assets/styles.css
new file mode 100644
index 0000000..37e8c62
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/assets/styles.css
@@ -0,0 +1,606 @@
+: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);
+ 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);
+}
+
+/* Header */
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ -webkit-app-region: drag; /* Draggable area */
+}
+
+.header-info {
+ text-align: center;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.app-title {
+ font-weight: 600;
+ font-size: 1rem;
+ color: var(--text-main);
+}
+
+.status-indicator {
+ font-size: 0.8rem;
+ color: var(--success);
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.status-dot {
+ width: 6px;
+ height: 6px;
+ background-color: var(--success);
+ border-radius: 50%;
+ box-shadow: 0 0 8px var(--success);
+}
+
+.icon-btn {
+ background: none;
+ border: none;
+ color: var(--text-main);
+ padding: 8px;
+ cursor: pointer;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ -webkit-app-region: no-drag; /* Buttons clickable */
+}
+
+.icon-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.header-buttons {
+ display: flex;
+ gap: 4px;
+ 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.1), rgba(255,255,255,0));
+ box-shadow: 5px 5px 15px rgba(0,0,0,0.1), inset 1px 1px 2px rgba(255,255,255,0.3);
+}
+
+.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);
+}
+
+.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;
+}
+
+/* Track Info */
+.track-info {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.track-info h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+.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;
+}
+
+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;
+}
+
+/* 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;
+}
diff --git a/src-tauri/gen/android/app/src/main/java/si/klevze/radioPlayer/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/si/klevze/radioPlayer/MainActivity.kt
new file mode 100644
index 0000000..0165eaf
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/java/si/klevze/radioPlayer/MainActivity.kt
@@ -0,0 +1,27 @@
+package si.klevze.radioPlayer
+
+import android.os.Bundle
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+
+ val webView = findViewById(R.id.webview)
+ webView.webViewClient = WebViewClient()
+ webView.webChromeClient = WebChromeClient()
+ webView.settings.javaScriptEnabled = true
+ webView.settings.domStorageEnabled = true
+ webView.settings.allowFileAccess = true
+
+ // Load the bundled web UI from assets/.
+ webView.loadUrl("file:///android_asset/index.html")
+ }
+}
diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..9170623
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..28f1aa1
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..85d0c88
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..28f1aa1
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..73e48db
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..13dd214
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..73e48db
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..1d98044
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a888b33
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1d98044
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..0818324
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a2a838e
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0818324
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b18bceb
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3f8a57f
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b18bceb
Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..80ec7e3
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src-tauri/gen/android/app/src/main/res/values/colors.xml b/src-tauri/gen/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/res/values/strings.xml b/src-tauri/gen/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..41ef4ea
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ RadioPlayer
+ RadioPlayer
+
\ No newline at end of file
diff --git a/src-tauri/gen/android/app/src/main/res/values/themes.xml b/src-tauri/gen/android/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..80ec7e3
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..782d63b
--- /dev/null
+++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts
new file mode 100644
index 0000000..607240b
--- /dev/null
+++ b/src-tauri/gen/android/build.gradle.kts
@@ -0,0 +1,22 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath("com.android.tools.build:gradle:8.11.0")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+tasks.register("clean").configure {
+ delete("build")
+}
+
diff --git a/src-tauri/gen/android/buildSrc/build.gradle.kts b/src-tauri/gen/android/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..5c55bba
--- /dev/null
+++ b/src-tauri/gen/android/buildSrc/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ `kotlin-dsl`
+}
+
+gradlePlugin {
+ plugins {
+ create("pluginsForCoolKids") {
+ id = "rust"
+ implementationClass = "RustPlugin"
+ }
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ compileOnly(gradleApi())
+ implementation("com.android.tools.build:gradle:8.11.0")
+}
+
diff --git a/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/BuildTask.kt b/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/BuildTask.kt
new file mode 100644
index 0000000..5bc2e2b
--- /dev/null
+++ b/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/BuildTask.kt
@@ -0,0 +1,68 @@
+import java.io.File
+import org.apache.tools.ant.taskdefs.condition.Os
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.logging.LogLevel
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+
+open class BuildTask : DefaultTask() {
+ @Input
+ var rootDirRel: String? = null
+ @Input
+ var target: String? = null
+ @Input
+ var release: Boolean? = null
+
+ @TaskAction
+ fun assemble() {
+ val executable = """node""";
+ try {
+ runTauriCli(executable)
+ } catch (e: Exception) {
+ if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+ // Try different Windows-specific extensions
+ val fallbacks = listOf(
+ "$executable.exe",
+ "$executable.cmd",
+ "$executable.bat",
+ )
+
+ var lastException: Exception = e
+ for (fallback in fallbacks) {
+ try {
+ runTauriCli(fallback)
+ return
+ } catch (fallbackException: Exception) {
+ lastException = fallbackException
+ }
+ }
+ throw lastException
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ fun runTauriCli(executable: String) {
+ val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
+ val target = target ?: throw GradleException("target cannot be null")
+ val release = release ?: throw GradleException("release cannot be null")
+ val args = listOf("tauri", "android", "android-studio-script");
+
+ project.exec {
+ workingDir(File(project.projectDir, rootDirRel))
+ executable(executable)
+ args(args)
+ if (project.logger.isEnabled(LogLevel.DEBUG)) {
+ args("-vv")
+ } else if (project.logger.isEnabled(LogLevel.INFO)) {
+ args("-v")
+ }
+ if (release) {
+ args("--release")
+ }
+ args(listOf("--target", target))
+ }.assertNormalExitValue()
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/RustPlugin.kt b/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/RustPlugin.kt
new file mode 100644
index 0000000..4aa7fca
--- /dev/null
+++ b/src-tauri/gen/android/buildSrc/src/main/java/si/klevze/radioPlayer/kotlin/RustPlugin.kt
@@ -0,0 +1,85 @@
+import com.android.build.api.dsl.ApplicationExtension
+import org.gradle.api.DefaultTask
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.get
+
+const val TASK_GROUP = "rust"
+
+open class Config {
+ lateinit var rootDirRel: String
+}
+
+open class RustPlugin : Plugin {
+ private lateinit var config: Config
+
+ override fun apply(project: Project) = with(project) {
+ config = extensions.create("rust", Config::class.java)
+
+ val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
+ val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
+
+ val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
+ val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
+
+ val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
+
+ extensions.configure {
+ @Suppress("UnstableApiUsage")
+ flavorDimensions.add("abi")
+ productFlavors {
+ create("universal") {
+ dimension = "abi"
+ ndk {
+ abiFilters += abiList
+ }
+ }
+ defaultArchList.forEachIndexed { index, arch ->
+ create(arch) {
+ dimension = "abi"
+ ndk {
+ abiFilters.add(defaultAbiList[index])
+ }
+ }
+ }
+ }
+ }
+
+ afterEvaluate {
+ for (profile in listOf("debug", "release")) {
+ val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
+ val buildTask = tasks.maybeCreate(
+ "rustBuildUniversal$profileCapitalized",
+ DefaultTask::class.java
+ ).apply {
+ group = TASK_GROUP
+ description = "Build dynamic library in $profile mode for all targets"
+ }
+
+ tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
+
+ for (targetPair in targetsList.withIndex()) {
+ val targetName = targetPair.value
+ val targetArch = archList[targetPair.index]
+ val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
+ val targetBuildTask = project.tasks.maybeCreate(
+ "rustBuild$targetArchCapitalized$profileCapitalized",
+ BuildTask::class.java
+ ).apply {
+ group = TASK_GROUP
+ description = "Build dynamic library in $profile mode for $targetArch"
+ rootDirRel = config.rootDirRel
+ target = targetName
+ release = profile == "release"
+ }
+
+ buildTask.dependsOn(targetBuildTask)
+ tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
+ targetBuildTask
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/gen/android/gradle.properties b/src-tauri/gen/android/gradle.properties
new file mode 100644
index 0000000..2a7ec69
--- /dev/null
+++ b/src-tauri/gen/android/gradle.properties
@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c5f9a53
--- /dev/null
+++ b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue May 10 19:22:52 CST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/src-tauri/gen/android/gradlew b/src-tauri/gen/android/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/src-tauri/gen/android/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/src-tauri/gen/android/gradlew.bat b/src-tauri/gen/android/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/src-tauri/gen/android/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle
new file mode 100644
index 0000000..44ba5db
--- /dev/null
+++ b/src-tauri/gen/android/settings.gradle
@@ -0,0 +1,3 @@
+include ':app'
+
+rootProject.name = 'radio_tauri'
diff --git a/tools/post-build-rcedit.js b/tools/post-build-rcedit.js
index d2fe892..992ab39 100644
--- a/tools/post-build-rcedit.js
+++ b/tools/post-build-rcedit.js
@@ -19,14 +19,26 @@ if (!fs.existsSync(iconPath)) {
console.log('Patching EXE icon with rcedit...');
-// Prefer local installed binary (node_modules/.bin/rcedit) to avoid relying on npx in some CI/envs
-const localBin = path.join(repoRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'rcedit.exe' : 'rcedit');
+// Prefer local installed binary (node_modules/.bin) to avoid relying on npx.
+// On Windows, npm typically creates a .cmd shim, which Node can execute.
+const binDir = path.join(repoRoot, 'node_modules', '.bin');
+const localCandidates = process.platform === 'win32'
+ ? [
+ path.join(binDir, 'rcedit.cmd'),
+ path.join(binDir, 'rcedit.exe'),
+ path.join(binDir, 'rcedit'),
+ ]
+ : [path.join(binDir, 'rcedit')];
+
+const localBin = localCandidates.find(p => fs.existsSync(p));
+
let cmd, args;
-if (fs.existsSync(localBin)) {
+if (localBin) {
cmd = localBin;
args = [exePath, '--set-icon', iconPath];
} else {
- // fallback to npx
+ // Fallback to npx. Note: Node can't execute PowerShell shims (npx.ps1), so this may fail
+ // in environments that only provide .ps1 launchers.
cmd = 'npx';
args = ['rcedit', exePath, '--set-icon', iconPath];
}
@@ -35,7 +47,8 @@ const res = spawnSync(cmd, args, { stdio: 'inherit' });
if (res.error) {
console.error(`Failed to run ${cmd}:`, res.error.message);
- console.error('Ensure rcedit is installed (npm install --save-dev rcedit) or that npx is available.');
+ console.error('Ensure rcedit is installed and available as a .cmd/.exe in node_modules/.bin (run `npm install`).');
+ console.error('If you rely on npx, make sure you have npx.cmd on PATH (PowerShell-only shims like npx.ps1 will not work with Node spawn).');
process.exit(1);
}