Live & Limited Commercials

Your Station Name
or Tag Here

tag line here

0
Stations
0
Hosts
0
Shows
Our Network

All Stations

⭐ Station of the Day Loading… Play ▶
Features

Built for Listeners

🎵

Limited Commercials

Only 3 minutes per hour with long music sweeps. No endless ad breaks. The way radio should be.

📡

Live Stations

Hand-picked music across every genre and decade.

📱

Listen Anywhere

This site IS the app. Install it on any device. One tap and you're in.

⏱️

Sleep Timer & Alarm

Fall asleep to your favorite station, wake up to it. Built right into the player.

🎤

Voice Messages

Record and send a voice message directly to any station. 30 seconds of your voice, on the air.

🔖

Save That Song

Hear something amazing? One tap saves it. Never lose a great discovery again.

Programming

Shows

0:00 0:00
On Air Talent

Our Hosts

Over 40 broadcasters across the Customer Station. Real voices, real personalities, real radio. Click any card to read their full bio.

Now Playing

Jukebox

See what's playing across every station, updated near real-time.

Loading...

Watch

Videos

Live Feed

News

The latest headlines — updated every 15 minutes.

Tap "News" to load the feed.
Music News
Live headlines from 25+ major music publications. Click the News tab in the nav to load the latest.

About Us

No about sections configured yet. Add them in the admin panel under Your Brand.

Get in Touch

Contact Us

Have a question, feedback, or want to join the network? Drop us a message and we'll get back to you.

Syndication

Syndicated Show Demos

All 12 syndicated shows available for your station. Contact us for licensing and scheduling.

LIVE
⭐ STATION OF THE DAY Loading... ▶ Play

Stations

Jukebox

Our Hosts

Shows

Tap a station to start
Customer Station

Song Request

Volume
Play a station to see recent tracks
📻
Select a Station
Customer Station
Tap a station below to start

Privacy Policy

Terms of Service

CA Privacy Rights

Contest Rules

Play a station to see recent tracks
📻
Select a Station
Customer Station
Tap a station to start
Mini Player
No Station
---
---
📻
Install Customer Station
Add to your home screen for one-tap access

⏪ Instant Replay

🎙 Send a Voice Message

Recording for the station

Record up to 30 seconds. We'll email it to the station. Email is optional — only include it if you want the station to follow up with you.

0:00
30 seconds max

🎙 Send a Voice Message

No station is playing right now. Pick one to leave your message for.

✉ Send a Message

✉ Send a Message

Send a message to the station

Sending to: Select a station
Email is optional — only include it if you want the station to follow up with you.

⏰ Alarm Radio

No alarms set
New Alarm

Station Notes

Welcome to Customer Station

Email is optional — enter it to unlock cross-device listener history and saved-song sync, or skip ahead to install. We'll never sell or share your email — promise.
✓We never sell your email
✓We never share with third parties
✓Saved songs & favorites sync across all your devices
✓Unsubscribe any time
🎉
Customer Station is installed!
One more step to put it on your desktop.
Add to desktop — Windows
Browsers don't let websites create desktop icons automatically — this is a 10-second one-time fix.

💰 Buy This Song

Image
—
—
Live Listener Count
Pulling live data from Live365…

Keyboard Shortcuts

Play / PauseSpace
Mute / UnmuteM
Next Station→
Previous Station←
Volume Up↑
Volume Down↓
Save SongS
Instant ReplayR
Show Shortcuts?

📲 Install as an App

Get a home-screen icon, lock-screen controls, and offline caching.


Want all your devices coordinated? Enter your email to register or log in.

Tap a station to start
Customer Station

GPTAssistant

Pick a station above to see what's playing, and I'll tell you all about the song!
'; try{frame.removeAttribute('src');}catch(e){} frame.srcdoc=doc; } pop.style.display='flex'; document.body.classList.add('more-popup-open'); } function _closeMorePopup(){ var pop=document.getElementById('moreAppPopup');if(!pop)return; var frame=document.getElementById('moreAppPopupFrame'); if(frame){try{frame.removeAttribute('srcdoc');}catch(e){}frame.src='about:blank';frame.style.display='none';} var html=document.getElementById('moreAppPopupHtml'); if(html){html.innerHTML='';html.style.display='none';} pop.style.display='none'; document.body.classList.remove('more-popup-open'); } window.closeMorePopup=_closeMorePopup; function _moreItemClick(it){ if(!it)return; var label=it.label||''; if(it.type==='page'&&it.target){ // Open internal page inside popup. Use ?page= query param + an // iframe load handler that postMessages the target page, so we // don\'t rely on the hash router firing at exactly the right // time during iframe boot. var base=location.pathname.replace(/[^\/]*$/,'')+'index.html'; var url=base+'?embed=1&page='+encodeURIComponent(it.target); _openMorePopup(label||'Page','iframe',url); return; } if(it.type==='url'&&it.target){ // External URLs: route through the in-page story overlay. It has // an "Open in new tab" button always-visible up top so the user // has an out if the destination blocks iframe embedding (linktr.ee, // Google, FB, news with strict CSP, etc.). if (typeof window.openStoryOverlay === 'function') { window.openStoryOverlay(it.target); } else { _openMorePopup(label||'Link','iframe',it.target); } return; } if(it.type==='widget'&&it.target){ _openMorePopup(label||'Widget','html',it.target); return; } /* RF_MORE_FEATURE_CLICK — invoke built-in feature via the shared handler map. Close the More screen first so the feature's own modal/panel isn't trapped behind it. */ if(it.type==='feature'&&it.target){ var fn = (window._RF_FEATURE_HANDLERS||{})[it.target]; if(typeof fn === 'function'){ try { if(typeof maSwitchTab==='function') maSwitchTab('home'); } catch(e){} setTimeout(function(){ try { fn(); } catch(e){ console.log('[more feature]', e); } }, 80); } return; } } window.maHandleMore=function(){ _applyMoreLabel(); _renderMoreList(); if(typeof maSwitchTab==='function')maSwitchTab('more'); }; window._applyMoreButtonLabel=_applyMoreLabel; // Open an in-app page (or a home section) inside the full-screen "Back to the Music" // overlay using the embed iframe. Keeps the user inside the app instead of dumping // them onto the public website. Used by mobile tab-bar page:/section: actions. window._rfOpenAppPage=function(pageId,label,sectionId){ try{ var base=location.pathname.replace(/[^\/]*$/,'')+'index.html'; var url=base+'?embed=1&page='+encodeURIComponent(pageId)+(sectionId?'&scroll='+encodeURIComponent(sectionId):''); _openMorePopup(label||'','iframe',url); }catch(e){console.log('[openAppPage]',e);} }; // Open a home-page widget's raw HTML inside the in-app popup. Used by the // mobile tab-bar widget: action. Mirrors the More-menu 'html' path. window._rfOpenAppWidget=function(label,html){ try{ _openMorePopup(label||'Widget','html',html); } catch(e){console.log('[openAppWidget]',e);} }; // Initial label sync after data loads try{_applyMoreLabel();}catch(e){} // ── Car Mode ── function initMaCar(){ var grid=document.getElementById('maCarStations');if(!grid)return; // Always rebuild so favorite reordering is reflected each time car mode opens grid.innerHTML=''; var _mcFavs=[];try{_mcFavs=JSON.parse(localStorage.getItem('site-favorites')||'[]')}catch(e){} var _mcFavSet=_mcFavs.reduce(function(m,n){m[n]=true;return m;},{}); var _mcFavOrdered=[], _mcRest=[]; STATIONS.forEach(function(s){(_mcFavSet[s.name] ? _mcFavOrdered : _mcRest).push(s);}); var _mcList=_mcFavOrdered.concat(_mcRest); _mcList.forEach(function(s){ var btn=document.createElement('button'); btn.style.cssText='width:100%;aspect-ratio:1;border-radius:50%;overflow:hidden;cursor:pointer;border:2px solid var(--border);background:var(--bg3);padding:0;transition:transform .12s,border-color .2s'; btn.dataset.name=s.name; btn.innerHTML=s.logo?''+s.name+'':''+s.name.substring(0,2)+''; btn.title=s.name; btn.onclick=function(){playStation(s.name);setTimeout(syncCarDisplay,300)}; grid.appendChild(btn); }); syncCarDisplay(); if(!window._maCarSync)window._maCarSync=setInterval(syncCarDisplay,3000); } window.initMaCar=initMaCar; function syncCarDisplay(){ var sEl=document.getElementById('maCarStation'); var icon=document.getElementById('maCarPlayIcon'); var pbIcon=document.getElementById('pbIcon'); if(currentStn&&sEl){ sEl.textContent=currentStn.name; document.querySelectorAll('#maCarStations button').forEach(function(b){ var active=b.dataset.name===currentStn.name; b.style.borderColor=active?'var(--accent)':'var(--border)'; b.style.boxShadow=active?'0 0 12px var(--accent-glow)':'none'; }); } else if(sEl){ sEl.textContent='Tap a station to start'; } if(pbIcon&&icon)icon.innerHTML=pbIcon.innerHTML; } window.maCarNext=function(){ if(!STATIONS.length)return; var idx=currentStn?STATIONS.indexOf(currentStn):-1; idx=(idx+1)%STATIONS.length; playStation(STATIONS[idx].name);setTimeout(syncCarDisplay,500); }; window.maCarPrev=function(){ if(!STATIONS.length)return; var idx=currentStn?STATIONS.indexOf(currentStn):-1; idx=(idx-1+STATIONS.length)%STATIONS.length; playStation(STATIONS[idx].name);setTimeout(syncCarDisplay,500); }; // ── Toast ── window.maShowToast=function(msg){ var t=document.getElementById('maToast'); if(!t) return; t.textContent=msg; t.classList.add('show'); setTimeout(function(){t.classList.remove('show')},2500); }; // ── Sync now-playing from desktop player every 2s ── function syncNowPlaying(){ var pbTitle=document.getElementById('pbTitle'); var pbArtist=document.getElementById('pbArtist'); var pbStn=document.getElementById('pbStn'); var pbArt=document.getElementById('pbArt'); var pbIcon=document.getElementById('pbIcon'); var pbEq=document.getElementById('pbEq'); var pbVs=document.getElementById('pbVs'); var maNpTitle=document.getElementById('maNpTitle'); var maNpArtist=document.getElementById('maNpArtist'); var maNpStation=document.getElementById('maNpStation'); var maNpArt=document.getElementById('maNpArt'); var maPlayIcon=document.getElementById('maPlayIcon'); var maEqBars=document.getElementById('maEqBars'); var maVolSlider=document.getElementById('maVolSlider'); if(pbTitle&&maNpTitle) maNpTitle.textContent=pbTitle.textContent; if(pbArtist&&maNpArtist) maNpArtist.textContent=pbArtist.textContent; if(pbStn&&maNpStation) maNpStation.textContent=pbStn.textContent; /* RF_CAR_NP — song + artist text under the car-mode volume slider. Blank until a station is actually chosen so the idle placeholder copy ("Customer Station" / "Tap a station...") never shows there. */ var carSong=document.getElementById('maCarSong'); var carArtist=document.getElementById('maCarArtist'); if(carSong&&carArtist){ var idle=!pbStn||/select a station/i.test(pbStn.textContent); carSong.textContent=idle?'':(pbTitle?pbTitle.textContent:''); carArtist.textContent=idle?'':(pbArtist?pbArtist.textContent:''); } // Sync album art if(pbArt&&maNpArt){ var artImg=pbArt.querySelector('img'); var maArtImg=maNpArt.querySelector('img'); if(artImg){ if(!maArtImg){ maNpArt.innerHTML=''; } else if(maArtImg.src!==artImg.src){ maArtImg.src=artImg.src; } } } // Sync play/pause icon if(pbIcon&&maPlayIcon){ maPlayIcon.innerHTML=pbIcon.innerHTML; } // Sync EQ bars animation state if(pbEq&&maEqBars){ if(pbEq.classList.contains('paused')){ maEqBars.classList.add('paused'); } else { maEqBars.classList.remove('paused'); } } // Sync volume if(pbVs&&maVolSlider&&document.activeElement!==maVolSlider){ maVolSlider.value=pbVs.value; } // Sync mute state var pbVi=document.getElementById('pbVi'); var maMuteBtn=document.getElementById('maMuteBtn'); if(pbVi&&maMuteBtn){ var isMuted=pbVi.textContent.indexOf('\uD83D\uDD07')>-1||pbVi.textContent.indexOf('🔇')>-1; if(isMuted){ maMuteBtn.classList.add('muted'); maMuteBtn.innerHTML='🔇'; } else { maMuteBtn.classList.remove('muted'); maMuteBtn.innerHTML='🔊'; } } // Highlight active station card in mobile/PWA. Read from window.currentStn // first so this works regardless of whether the desktop .sc cards are // present/visible (e.g., text-hero layouts may not render .sc, in which // case the old .sc.playing query returned nothing and the ON AIR pill // never lit up on the mobile card). var activeName = (window.currentStn && window.currentStn.name) || ''; if (!activeName) { var activeDesktop=document.querySelector('.sc.playing'); if(activeDesktop) activeName=activeDesktop.dataset.name||''; } document.querySelectorAll('.ma-stn-card').forEach(function(c){ c.classList.toggle('active',c.dataset.name===activeName); }); // Mobile/PWA jukebox cards (.majb-d-card) use the .playing class for // the ON AIR pill — keep them in sync with the same activeName. document.querySelectorAll('.majb-d-card').forEach(function(c){ c.classList.toggle('playing', c.dataset.name===activeName); }); // Desktop Jukebox PAGE cards (.jb-item) use the same .playing class. document.querySelectorAll('.jb-item').forEach(function(c){ c.classList.toggle('playing', c.dataset.name===activeName); }); // Desktop NP-dashboard cards (.npd-item — Now Playing dashboard on the // home page below the hero) use the same .playing class. document.querySelectorAll('.npd-item').forEach(function(c){ c.classList.toggle('playing', c.dataset.name===activeName); }); // Sync replay button state var desktopReplay=document.getElementById('pbReplay'); var maReplay=document.getElementById('maReplayBtn'); if(desktopReplay&&maReplay){ if(desktopReplay.classList.contains('disabled')){ maReplay.classList.add('disabled'); } else { maReplay.classList.remove('disabled'); } } // Sync SOTD — only copy from desktop if it has actually settled. // Otherwise we\'d clobber the value cloneStationsToMobile already // calculated locally from the owned[] list (this was the iPhone bug // where SOTD kept reverting to "Loading…" every 2s). var desktopSotd=document.getElementById('sotdName'); var maSotdName=document.getElementById('maSotdName'); if(desktopSotd && maSotdName){ var dt = (desktopSotd.textContent||'').trim(); var isLoading = !dt || /^Loading[\.…]*$/i.test(dt); if (!isLoading) maSotdName.textContent = dt; else if (window._sotdStation) maSotdName.textContent = window._sotdStation; else if (window._siteData && window._siteData.stations) { var owned = window._siteData.stations.filter(function(s){return s && (s.type||'owned')==='owned' && s.active!==false;}); if (owned.length) { var idx = Math.floor(Date.now()/86400000) % owned.length; maSotdName.textContent = owned[idx].name; } } } } // ── Init ── cloneStationsToMobile(); buildGenreFilter(); populateReqStations(); syncNowPlaying(); setInterval(syncNowPlaying,2000); // Close menus on outside tap document.addEventListener('click',function(e){ if(!e.target.closest('.ma-sleep-wrap')){ var sm=document.getElementById('maSleepMenu'); if(sm) sm.classList.remove('show'); } if(!e.target.closest('.ma-eq-wrap')){ var em=document.getElementById('maEqMenu'); if(em) em.classList.remove('show'); } if(!e.target.closest('.ma-rp-wrap')){ var rm=document.getElementById('maRpMenu'); if(rm) rm.classList.remove('show'); } }); /* First-visit iOS install hint. Apple blocks automatic PWA install on iPhone, so brand-new iOS visitors would never discover the Add to Home Screen path on their own. Show a one-time gentle prompt a few seconds after load, dismissible, cached in localStorage so it never repeats. Only fires if not already installed. */ try{ var onIOS=(typeof _piDetectOS==='function')&&_piDetectOS()==='ios'; var alreadyInstalled=(typeof _piIsStandalone==='function')&&_piIsStandalone(); var hintSeen=false; try{hintSeen=!!localStorage.getItem('site-ios-install-hint-seen')}catch(_){hintSeen=false} if(onIOS&&!alreadyInstalled&&!hintSeen){ setTimeout(function(){ if(typeof showPostInstallHelp==='function')showPostInstallHelp('manual'); try{localStorage.setItem('site-ios-install-hint-seen','1')}catch(_){} },4000); } }catch(_){} /* Desktop App: restore the saved Stations|Jukebox view (default Jukebox) so the popup opens straight to the jukebox grid the user expects. Detect via the ?desktop=1 URL param (not the body class) because _detectDesktopPwa hasn't added body.desktop-pwa yet at this point — it runs on DOMContentLoaded. Schedule the switch for the same event so the class IS set by the time loadMaJukebox checks it. */ try{ // The toggle bar and default-to-Jukebox apply whenever the mobile-app // shell is on screen (mobile OR Desktop App popup). The colorful action // bar (Stations / Ways to Listen / Voicemail / Requests / GPT) is // Desktop-App-only — gated on the ?desktop=1 URL param. var _rfIsDesktopApp=false; try{_rfIsDesktopApp=/[?&]desktop=1\b/.test(window.location.search)}catch(_){} console.log('[RF App Mode] init, desktopApp=',_rfIsDesktopApp); if(true){ var _rfMountTopBar=function(){ // Inject the shared app-mode styles (white tabs, etc). The toggle // itself now lives inside the jukebox + home screen headers (see // _rfInjectScreenToggles), not in a fixed top bar. try{ _rfDvEnsureStyles(); if(document.getElementById('rf-app-mode-styles'))return; var pushS=document.createElement('style'); pushS.id='rf-app-mode-styles'; pushS.textContent='body.rf-app-mode .ma-tab{color:#fff !important;}' +'body.rf-app-mode .ma-tab span{color:#fff !important;}' +'body.rf-app-mode .ma-tab.active,body.rf-app-mode .ma-tab.active span{color:var(--accent) !important;}' /* Reserve a consistent header zone at the top of each screen for the toggle */ +'.rf-screen-toggle{display:flex;justify-content:center;align-items:center;' +'padding:12px 12px 4px;}' +'@media(max-width:768px){.rf-screen-toggle{padding:10px 12px 2px;}}' /* Hide the animated EQ "musical bars" in the player bar — takes up space and offers little next to the play/volume controls. */ +'body.rf-app-mode .ma-eq-bars{display:none !important;}' /* Hide the "Recently Played" button in the player-bar popup tray. */ +'body.rf-app-mode #maRpBtn{display:none !important;}'; document.head.appendChild(pushS); console.log('[RF App Mode] styles injected'); }catch(e){console.log('[RF App Mode] styles err',e);} }; /* Inject the Stations|Jukebox toggle at the top of both the home/stations screen and the jukebox screen. On the jukebox screen, also remove the old "← Return" button (the toggle does the same job and more). */ var _rfInjectScreenToggles=function(){ try{ // Jukebox screen — replace Return button with the toggle. var jbScreen=document.getElementById('ma-jukebox'); if(jbScreen && !jbScreen.querySelector('.rf-sj-toggle')){ var jbHdr=jbScreen.firstElementChild; if(jbHdr){ var returnBtn=jbHdr.querySelector('button'); if(returnBtn) returnBtn.parentNode.removeChild(returnBtn); var tgJb=_rfDvBuildToggle(); jbHdr.insertBefore(tgJb, jbHdr.firstChild); } } // Home/stations screen — toggle lives inline in the genre filter // row (next to the Genre dropdown), inserted by buildGenreFilter. // Also clean up any old standalone toggle row from prior versions. var oldRow=document.querySelector('#ma-home > .rf-screen-toggle'); if(oldRow) oldRow.parentNode.removeChild(oldRow); _rfDvSyncToggles(_rfDvGet()); }catch(e){console.log('[RF App Mode] screen toggle err',e);} }; var _rfApplyView=function(){ try{ var _rfV=_rfDvGet(); console.log('[RF Desktop App] applying view',_rfV); maSwitchTab(_rfV==='jukebox'?'jukebox':'home'); }catch(e){console.log('[RF Desktop App] apply err',e);} }; /* Stations popup — Desktop App: a centered overlay listing all stations with their logo + name. Click a tile to play; click outside or hit × to close. Built once, then just toggled. */ function _rfOpenStationsPopup(){ try{ var ov=document.getElementById('rf-stn-overlay'); if(!ov){ if(!document.getElementById('rf-stn-styles')){ var st=document.createElement('style');st.id='rf-stn-styles'; st.textContent= '#rf-stn-overlay{position:fixed;inset:0;background:rgba(5,5,16,.78);' +'backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);' +'z-index:99999;display:none;align-items:flex-start;justify-content:center;' +'padding:60px 24px 24px;overflow-y:auto;}' +'#rf-stn-overlay.show{display:flex;}' +'#rf-stn-panel{background:#0a0a0a;border:1px solid rgba(255,255,255,.08);' +'border-radius:18px;max-width:1100px;width:100%;box-shadow:0 24px 64px rgba(0,0,0,.6);' +'overflow:hidden;animation:rfStnIn .22s ease;}' +'@keyframes rfStnIn{from{opacity:0;transform:translateY(-12px) scale(.98)}to{opacity:1;transform:none}}' +'#rf-stn-head{display:flex;align-items:center;justify-content:space-between;' +'padding:18px 24px;border-bottom:1px solid rgba(255,255,255,.06);' +'background:linear-gradient(135deg,rgba(251,146,60,.08),rgba(251,146,60,.02));}' +'#rf-stn-head h3{margin:0;font-family:"Sora",inherit;font-size:18px;font-weight:800;' +'color:#fdba74;letter-spacing:.5px;}' +'#rf-stn-close{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);' +'color:#fff;width:32px;height:32px;border-radius:50%;cursor:pointer;font-size:18px;' +'line-height:1;display:flex;align-items:center;justify-content:center;' +'transition:all .15s;font-family:inherit;}' +'#rf-stn-close:hover{background:rgba(255,255,255,.12);transform:rotate(90deg);}' +'#rf-stn-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;' +'background:rgba(255,255,255,.06);}' +'@media(max-width:900px){#rf-stn-grid{grid-template-columns:repeat(2,1fr);}}' +'@media(max-width:600px){#rf-stn-grid{grid-template-columns:1fr;}}' +'.rf-stn-card{display:grid;grid-template-columns:72px 1fr auto;gap:14px;' +'padding:14px 18px;align-items:center;background:#0a0a0a;cursor:pointer;' +'border:none;text-align:left;color:#fff;font-family:inherit;transition:background .18s;}' +'.rf-stn-card:hover{background:#141422;}' +'.rf-stn-card.playing{background:linear-gradient(135deg,rgba(99,102,241,.15),rgba(99,102,241,.05));}' +'.rf-stn-art{width:72px;height:72px;border-radius:8px;overflow:hidden;background:var(--bg3);' +'display:flex;align-items:center;justify-content:center;font-family:"Sora";' +'font-weight:800;font-size:24px;color:#fff;flex-shrink:0;}' +'.rf-stn-art img{width:100%;height:100%;object-fit:contain;}' +'.rf-stn-body{min-width:0;}' +'.rf-stn-name{font-family:"Sora";font-size:14px;font-weight:700;color:#fff;' +'margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' +'.rf-stn-genre{font-family:"JetBrains Mono",monospace;font-size:9px;letter-spacing:.2em;' +'text-transform:uppercase;color:var(--c,#8a8a8a);white-space:nowrap;overflow:hidden;' +'text-overflow:ellipsis;font-weight:700;}' +'.rf-stn-play{width:36px;height:36px;border-radius:50%;background:rgba(99,102,241,.18);' +'border:1px solid rgba(99,102,241,.4);color:var(--accent2);display:flex;align-items:center;' +'justify-content:center;flex-shrink:0;transition:all .15s;}' +'.rf-stn-card:hover .rf-stn-play{background:var(--accent);color:#fff;border-color:var(--accent);}'; document.head.appendChild(st); } ov=document.createElement('div'); ov.id='rf-stn-overlay'; ov.innerHTML='
' +'

\u{1F4FB} All Stations

' +'
' +'
'; document.body.appendChild(ov); ov.addEventListener('click',function(e){ if(e.target===ov){ ov.classList.remove('show'); } if(e.target.id==='rf-stn-close'){ ov.classList.remove('show'); } }); document.addEventListener('keydown',function(e){ if(e.key==='Escape' && ov.classList.contains('show')) ov.classList.remove('show'); }); } // Rebuild grid each open so order/now-playing reflect latest state. var grid=ov.querySelector('#rf-stn-grid'); grid.innerHTML=''; (window.STATIONS||[]).forEach(function(s){ var card=document.createElement('button'); card.type='button'; card.className='rf-stn-card'; if(window.currentStn===s.name)card.classList.add('playing'); if(s.color)card.style.setProperty('--c',s.color); var initial=(s.name||'?').trim().charAt(0).toUpperCase(); var logo=s.logo ? '' : initial; card.innerHTML='
'+logo+'
' +'
'+s.name+'
' +'
'+((s.genre||'').split(',')[0]||'Live')+'
' +'
'; card.addEventListener('click',function(){ try{playStation(s.name);}catch(e){} ov.classList.remove('show'); }); grid.appendChild(card); }); ov.classList.add('show'); }catch(e){console.log('[RF Desktop App] stations popup err',e);} } var _rfMountActionBar=function(){ if(!_rfIsDesktopApp)return; try{ if(document.getElementById('rf-action-group'))return; var row=document.querySelector('.mobile-app .ma-pb-row')||document.querySelector('.ma-pb-row'); if(!row)return; if(!document.getElementById('rf-action-bar-styles')){ var st=document.createElement('style');st.id='rf-action-bar-styles'; st.textContent= '#rf-action-group{display:inline-flex;align-items:center;gap:8px;' +'margin:0 12px;flex-shrink:1;flex-wrap:nowrap;min-width:0;}' +'.rf-act{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;' +'border-radius:22px;border:1px solid transparent;background:rgba(255,255,255,.04);' +'font-family:"Sora","DM Sans",inherit;font-size:11px;font-weight:700;' +'letter-spacing:.4px;cursor:pointer;transition:all .2s ease;white-space:nowrap;' +'line-height:1;flex-shrink:0;}' +'.rf-act:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,0,0,.45);}' +'.rf-act .rf-act-ic{font-size:13px;line-height:1;}' +'.rf-act-orange{color:#fdba74;border-color:rgba(251,146,60,.45);' +'background:linear-gradient(135deg,rgba(251,146,60,.18),rgba(249,115,22,.08));}' +'.rf-act-orange:hover{background:linear-gradient(135deg,rgba(251,146,60,.3),rgba(249,115,22,.18));border-color:#fb923c;color:#fff;}' +'.rf-act-cyan{color:#67e8f9;border-color:rgba(34,211,238,.45);' +'background:linear-gradient(135deg,rgba(34,211,238,.18),rgba(6,182,212,.08));}' +'.rf-act-cyan:hover{background:linear-gradient(135deg,rgba(34,211,238,.3),rgba(6,182,212,.18));border-color:#22d3ee;color:#fff;}' +'.rf-act-red{color:#fca5a5;border-color:rgba(248,113,113,.45);' +'background:linear-gradient(135deg,rgba(248,113,113,.18),rgba(239,68,68,.08));}' +'.rf-act-red:hover{background:linear-gradient(135deg,rgba(248,113,113,.3),rgba(239,68,68,.18));border-color:#f87171;color:#fff;}' +'.rf-act-green{color:#6ee7b7;border-color:rgba(52,211,153,.45);' +'background:linear-gradient(135deg,rgba(52,211,153,.18),rgba(16,185,129,.08));}' +'.rf-act-green:hover{background:linear-gradient(135deg,rgba(52,211,153,.3),rgba(16,185,129,.18));border-color:#34d399;color:#fff;}' +'.rf-act-purple{color:#c4b5fd;border-color:rgba(167,139,250,.45);' +'background:linear-gradient(135deg,rgba(167,139,250,.18),rgba(139,92,246,.08));}' +'.rf-act-purple:hover{background:linear-gradient(135deg,rgba(167,139,250,.3),rgba(139,92,246,.18));border-color:#a78bfa;color:#fff;}' /* Let the now-playing info shrink instead of pushing the actions off-screen */ +'.mobile-app .ma-pb-row .ma-np-info{min-width:0;flex-shrink:1;}'; document.head.appendChild(st); } var group=document.createElement('div'); group.id='rf-action-group'; // Shared feature → handler map for every assignable action. window._RF_FEATURE_HANDLERS = window._RF_FEATURE_HANDLERS || { stations: function(){ if(typeof _rfOpenStationsPopup==='function')_rfOpenStationsPopup(); }, ways: function(){ if(typeof openWaysToListen==='function')openWaysToListen(); }, voicemail: function(){ if(typeof openVoiceMessage==='function')openVoiceMessage(); }, requests: function(){ if(typeof openEmailMessage==='function')openEmailMessage(); }, gpt: function(){ if(typeof openGPTAssistant==='function')openGPTAssistant(); }, replay: function(){ if(typeof instantReplay==='function')instantReplay(); }, recent: function(btn){ window._rfFloatMenu('rpDropdown', btn); }, save: function(){ if(typeof saveSong==='function')saveSong(); }, saved: function(btn){ window._rfFloatMenu('savedPanel', btn, true); if(typeof renderSavedSongs==='function')renderSavedSongs(); }, eq: function(btn){ window._rfFloatMenu('eqMenu', btn); }, sleep: function(btn){ window._rfFloatMenu('sleepMenu', btn); }, alarm: function(){ if(typeof openAlarm==='function')openAlarm(); }, share: function(){ if(typeof sharePlayingStation==='function')sharePlayingStation(); }, car: function(){ if(typeof maSwitchTab==='function')maSwitchTab('car'); }, theme: function(){ if(typeof toggleTheme==='function')toggleTheme(); } }; // Defaults when no config.app_bar saved (today's 5 buttons). window._RF_BAR_DEFAULTS = window._RF_BAR_DEFAULTS || [ {id:'stations', label:'Stations', icon:'\u{1F4FB}', color:'#f97316'}, {id:'ways', label:'Ways to Listen', icon:'\u{1F4E1}', color:'#06b6d4'}, {id:'voicemail', label:'Voice Message', icon:'\u{1F399}', color:'#ef4444'}, {id:'requests', label:'Requests', icon:'✉', color:'#10b981'}, {id:'gpt', label:'GPT', icon:'\u{1F916}', color:'#8b5cf6'} ]; // (Re)build the on-bar buttons from config.app_bar. Off-bar features // are simply omitted here (they remain available in the ••• popup). window._rfBuildActionButtons = function(grp){ grp = grp || document.getElementById('rf-action-group'); if(!grp) return; var cfg = (window._siteData && window._siteData.config) || window._siteConfig || null; // Report #4 fix: this runs at boot BEFORE the config loader has // applied site-data, so it used to fall straight through to the // stock 5-button defaults. Read the locally cached site-data first // so a returning visitor always gets their station's custom bar. if(!cfg || !(Array.isArray(cfg.app_bar) && cfg.app_bar.length)){ try{ var _lsd=JSON.parse(localStorage.getItem('site-data')||'null'); if(_lsd && _lsd.config) cfg=_lsd.config; }catch(_){} } cfg = cfg || {}; var bar = (Array.isArray(cfg.app_bar) && cfg.app_bar.length) ? cfg.app_bar.filter(function(x){ return x && x.onBar !== false; }) : window._RF_BAR_DEFAULTS; grp.innerHTML=''; bar.forEach(function(item){ if(!item || !window._RF_FEATURE_HANDLERS[item.id]) return; var col = /^#[0-9a-fA-F]{6}$/.test(item.color||'') ? item.color : '#6366f1'; var btn=document.createElement('button'); btn.type='button'; btn.className='rf-act'; btn.setAttribute('data-act', item.id); btn.style.cssText='color:#fff;border:1px solid '+col+';background:'+col+';'; var ic; var _iv=String(item.icon||''); if(/^(https?:|\/|pictures\/|audio\/|app-icons\/)/i.test(_iv)||/\.(png|jpe?g|gif|webp|svg)$/i.test(_iv)){ ic=document.createElement('img'); ic.src=item.icon; ic.className='rf-act-ic'; ic.style.cssText='width:15px;height:15px;object-fit:contain;border-radius:3px;'; } else { ic=document.createElement('span'); ic.className='rf-act-ic'; ic.textContent=_iv; } var lb=document.createElement('span'); lb.textContent=item.label||item.id; btn.appendChild(ic); btn.appendChild(lb); grp.appendChild(btn); }); // Hide Stations when fewer than 4 active stations (hero already shows them). var stnBtn = grp.querySelector('button[data-act="stations"]'); if(stnBtn){ var cnt=0; if(window._siteData && Array.isArray(window._siteData.stations)) cnt=window._siteData.stations.filter(function(s){return s&&s.active!==false;}).length; else if(Array.isArray(window.STATIONS)) cnt=window.STATIONS.length; if(cnt>0) stnBtn.style.display = (cnt<4)?'none':''; } }; window._rfBuildActionButtons(group); group.addEventListener('click',function(e){ var b=e.target.closest('button[data-act]');if(!b)return; var fn=window._RF_FEATURE_HANDLERS[b.dataset.act]; try{ if(fn) fn(b); else console.log('[RF Action] no handler for',b.dataset.act); }catch(err){console.log('[RF Action] err',err);} }); // Insert inline in the player-bar row, right after the now-playing // info block (before the equalizer + transport controls). var info=row.querySelector('.ma-np-info'); if(info && info.nextSibling) row.insertBefore(group, info.nextSibling); else row.appendChild(group); // Clean up the old standalone bar if it ever got mounted. var oldBar=document.getElementById('rf-action-bar'); if(oldBar) oldBar.parentNode.removeChild(oldBar); // Hide the Stations button when fewer than 4 active stations — the // hero already shows them above the fold. Poll because the station // grid + window._siteData populate after data.json arrives, which // happens after this action bar mounts. var _rfStnBtnTries = 0; var _rfStnBtnTimer = setInterval(function(){ var stnBtn = group.querySelector('button[data-act="stations"]'); if (!stnBtn) { clearInterval(_rfStnBtnTimer); return; } var activeCount = 0; if (window._siteData && Array.isArray(window._siteData.stations)) { activeCount = window._siteData.stations.filter(function(s){ return s && s.active !== false; }).length; } else if (Array.isArray(window.STATIONS) && window.STATIONS.length) { activeCount = window.STATIONS.length; } if (activeCount > 0) { stnBtn.style.display = (activeCount < 4) ? 'none' : ''; clearInterval(_rfStnBtnTimer); } else if (++_rfStnBtnTries > 50) { // 50 × 200ms = 10s — give up; leave button visible by default. clearInterval(_rfStnBtnTimer); } }, 200); console.log('[RF Desktop App] action group mounted in player bar'); }catch(e){console.log('[RF Desktop App] action bar err',e);} }; var _rfBoot=function(){ _rfMountTopBar(); _rfInjectScreenToggles(); _rfMountActionBar(); _rfApplyView(); }; if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded',_rfBoot); }else{ setTimeout(_rfBoot,0); } } }catch(e){console.log('[RF Desktop App] init err',e);} } // Run mobile init on load if mobile width OR PWA standalone (desktop PWA reuses mobile-app shell) var _pwaStandalone=false;try{_pwaStandalone=window.matchMedia&&window.matchMedia('(display-mode: standalone)').matches}catch(_){_pwaStandalone=false} try{if(/[?&]desktop=1\b/.test(window.location.search))_pwaStandalone=true}catch(_){} if(window.innerWidth<=768||_pwaStandalone) initMobileApp(); // Re-init on resize crossing 768px threshold var _wasDesktop=window.innerWidth>768; window.addEventListener('resize',function(){ var isDesktop=window.innerWidth>768; if(_wasDesktop&&!isDesktop){ initMobileApp(); } _wasDesktop=isDesktop; }); })();
←Back to the Directory
×
🎵

—

Now Playing
—