# Changelog
All notable changes to **Radio** are documented here.
Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versioning: [SemVer](https://semver.org/).
---
## [Unreleased]
---
## [0.6.3] — 2026-05-30 — Discreet "buy me a coffee" support link
### Added
- New `RADIO_SUPPORT_URL` constant in `radio.php` (default `https://buymeacoffee.com/davidtkeane`). Wrapped in `if ( ! defined(...) )` so it's override-able from `wp-config.php`.
- Tiny footer line `☕ Like Radio? If You fancy to buy me a coffee →` inside the **Updates panel** on Settings, below the manual-update note.
- Matching footer line at the bottom of the **Credits + thanks** card on the About page.
- Both spots render from the same constant — change one place, both update.
- **Conditional render**: if `RADIO_SUPPORT_URL` is empty / undefined, the link is silently hidden. Forks can strip funding with one line.
### Design
- Muted styling deliberate: 12 px, admin-theme-coloured link, subtle top border. Reads as housekeeping ("here's where to send thanks") not a sales pitch. No yellow BMC brand chrome.
- Dark-theme variant for the divider (`#3c434a`) so it stays subtle on the dark surface.
**Files changed:** `radio.php` (version, `RADIO_SUPPORT_URL` constant), `inc/updater.php` (link inside the Updates panel after the manual-update paragraph), `inc/about.php` (link inside the Credits + thanks card; rotate v0.6.3 into latest expanded slot, v0.6.2 into earlier-releases list), `assets/css/radio.css` (`.radio-support-link` styling + dark-theme override).
---
## [0.6.2] — 2026-05-30 — Current version badge on Settings
### Added
- Small grey pill follows the **"Radio — Settings"** heading: `v{RADIO_VERSION}`. Visible at a glance so you don't have to hover the plugin row in *Plugins → Installed* or open *About* just to check what version you're on.
- Dark-theme variant of the badge (`#2c3338` background, `#c3c4c7` text) so it stays readable when `theme=dark`.
**Files changed:** `radio.php` (version), `inc/settings.php` (`v… ` inside the H1), `assets/css/radio.css` (`.radio-version-badge` styling + dark-theme override).
---
## [0.6.1] — 2026-05-30 — About page restructure
By v0.6.0 the About page had eight version-history entries, each a full paragraph, dwarfing the other cards and pushing Credits + thanks off the visible area. v0.6.1 rebalances the layout.
### Changed
- **Three short cards on top** (What / Who / **Credits**) — equal-height, balanced row. Credits is no longer a fourth card buried under the version history; it sits beside What and Who where it belongs.
- **Version history is its own full-width card below.** Only the **latest** release is shown in full; **earlier releases collapse to one line each** (version + date + headline). The card now stays compact however many versions ship — adding a future release adds one line, not a paragraph.
- **Full prose for older versions lives in `CHANGELOG.md` on Gitea** — the "View the full CHANGELOG.md →" link does the heavy lifting. Single source of truth, no duplication.
**Files changed:** `radio.php` (version), `inc/about.php` (3-card top + new `.radio-about-versions` block with `__latest` / `__earlier` sub-elements; 9 versions in the earlier-releases list incl. v0.1.0), `assets/css/radio.css` (removed dead `.radio-about-card--versions` rules; added `.radio-about-versions` + `__latest` + `__earlier` rules; dark-theme overrides for the new selectors).
---
## [0.6.0] — 2026-05-30 — Pop-out mini-player (continuous background play)
Until v0.5.0 the audio cut every time you navigated between WP admin pages — every navigation is a full page reload, which destroys the `` element. v0.6.0 fixes the background-music use case by letting you pop the player out into a separate browser window that persists across the parent tab's navigation.
### Added — **Pop out** button + standalone popup player
- Small **`↗ Pop out`** button beside the Play button on both the main Radio page and the Dashboard widget. Click it and a **380×560 standalone window** opens with just the player chrome (no WP admin sidebar / nav).
- The popup lives at `admin-post.php?action=radio_popout&play=1` — a new server-side route that renders a **full standalone HTML page** outside the WP admin shell (custom ``, head, body, no admin chrome).
- Popup is `radio_popout`-named so a second click on Pop out **re-focuses the existing window** instead of opening a new one.
- The popup's `` element is never destroyed by parent-tab navigation, so the music keeps playing while you click around Plugins, Posts, Users, etc.
### Auto-resume — pick up where the main tab left off
- The Pop-out button URL carries `&play=1`. `radio.js` reads it from the localized config and auto-calls `audio.play()` 200 ms after init. Same-origin user-gesture popups are exempt from autoplay-blocking on every modern browser, so it just works.
- On opening the popup, **every other audio surface in the main tab is paused** so you don't end up with two streams running simultaneously.
### Popup details
- Theme follows the user's saved choice (`radio_state['theme']`) — light by default, dark if explicitly set; the popup body gets the `radio-theme-dark` class so the existing dark-mode CSS rules apply.
- The popup includes everything you need to listen and switch: now-playing block (with dancing bars + Web Audio visualizer), play/pause, mute, volume, full station dropdown grouped by genre, error slot.
- Close button (`✕`) in the top-right calls `window.close()`.
- Pop out button **does not appear inside the popup** itself (would be infinite); the JS detects `popoutUrl === ''` in the localized config and hides any Pop-out button it finds.
- Popup blocked? The button shows a clear alert: *"Pop-out blocked by the browser. Allow popups for this site, then try again."*
### State stays in sync
- The popup uses the **same `radio_save_state` AJAX endpoint** as the main player. If you change station or volume in the popup, it persists to user_meta; the next time you load any Radio page in the main tab it picks up the new values.
- Track history continues to log from whichever surface is playing — the popup polls SomaFM and POSTs to `radio_log_track` exactly like the main player.
**Files changed:** `radio.php` (version, `popoutUrl` added to localized config, new `admin_post_radio_popout` action + `radio_render_popout_page` handler with full standalone HTML), `inc/admin-page.php` + `inc/dashboard-widget.php` (Pop-out button beside Play), `assets/css/radio.css` (Pop-out button styling), `assets/js/radio.js` (`bindPopOut` opens the window + pauses other surfaces, autoplay branch reads `cfg.autoPlay`), `inc/about.php` (history entry).
---
## [0.5.0] — 2026-05-29 — Track history + favourites
SomaFM plays deep cuts you'll never hear again. v0.5.0 quietly logs every track that scrolls past so you can find it again later — and a star button keeps the ones worth keeping forever.
### Added — `Radio → History` admin page (per-user)
- **History tab** — capped FIFO list of the last 500 played tracks. Each row shows when (relative time, full timestamp on hover), station, *artist — title*, four search links, and a favourite-star toggle.
- **Favourites tab** — uncapped list of starred tracks. Same row layout. Survives even when the history rolls over.
- **Filter** by artist/title (live, client-side) and by station (dropdown). **Clear history** button on the History tab — favourites preserved.
- **Four search providers** per row, brand-tinted on hover: **Spotify** (green), **YouTube** (red), **Apple Music** (pink), **Bandcamp** (teal). Deep-link search URLs only — no API keys, no third-party JS.
- Empty-state messages on both tabs.
### Added — automatic logging during playback
- `fetchTrack` (the existing 30s SomaFM polling loop) now hands every new track to a new `logTrackIfNew` helper that POSTs it to `wp_ajax_radio_log_track`.
- **Dedup**: client-side via `lastLoggedSig` so the 30s polling doesn't re-log the same song; server-side against the last entry in user_meta as belt-and-braces.
- **Junk filtering**: `(unknown)` artists and entries missing artist *or* title are dropped server-side in `radio_sanitize_entry`.
### Added — per-user storage
- Two new `user_meta` keys, separate from `radio_state` so frequent track inserts don't churn the player-state blob:
- `radio_history` — capped at **500 entries** (~50–80 KB max).
- `radio_favourites` — uncapped, expected to stay small (user-curated).
### Added — three new AJAX endpoints
- `wp_ajax_radio_log_track` — append a track (nonce: `radio_save_state`; player-page only).
- `wp_ajax_radio_toggle_favourite` — toggle a track in favourites (nonce: `radio_history`; History-page only).
- `wp_ajax_radio_clear_history` — clear the history list (nonce: `radio_history`; History-page only). Favourites untouched.
### Notes
- Per-user — nothing is shared, nothing leaves the site. Just your own listening history on your own WP account.
- **No PII concern** — entries are public station/artist/title strings from SomaFM's own JSON.
- Search-link UI tints toward each provider's brand colour on hover only — keeps the row visually calm in the default state.
**Files changed:** `radio.php` (version, require, submenu, asset enqueue hook, three AJAX endpoints, three new localized strings), **new** `inc/history.php` (storage helpers + page renderer), `assets/css/radio.css` (history-page table, toolbar, search-link pills, favourite star, dark-theme overrides), `assets/js/radio.js` (`logTrackIfNew` wired into `fetchTrack`; `bindHistoryPage` for filter/favourite/clear), `inc/about.php` (history entry).
---
## [0.4.0] — 2026-05-29 — Now-playing indicator: dancing bars + Web Audio visualizer
A small visual that instantly says *"this is playing right now."* Two layers — a reliable CSS-only indicator that always works, and a progressive Web Audio upgrade that draws actual frequency data when the browser allows.
### Added — dancing bars (always on, CSS only)
- Four tiny vertical bars next to the "Now Playing" label that pulse with a staggered `@keyframes` animation while the audio is playing, settling to a low static state when paused. Pure CSS — no JS dependency, no audio analysis.
- Bars use `var(--wp-admin-theme-color)` so they tint to whichever WP admin colour scheme the user has chosen.
- Driven by a single `.is-playing` class toggled on the `.radio-player` surface from the existing `play` / `pause` / `error` audio handlers.
### Added — Web Audio frequency visualizer (progressive upgrade)
- On first `play`, `tryVisualizer` builds an `AudioContext` + `AnalyserNode` chain on the `` element and starts drawing live frequency bars on a `` next to "Now Playing."
- `` now carries `crossorigin="anonymous"` so the Web Audio analyser can actually read the stream data (SomaFM serves the CORS headers).
- **Graceful fallback:** if `AudioContext` isn't available, or `createMediaElementSource` throws, or the analyser returns all-zeros for 2 s (CORS silently blocking), the visualizer state flips to `cors-blocked` / `init-failed` / `no-webaudio` and the CSS dancing bars remain — the plugin never loses its indicator.
- Canvas is sized to its CSS box × `devicePixelRatio` so it stays crisp on retina screens.
### State machine on `player._vizState`
| Value | Meaning |
|---|---|
| _undefined_ | not yet attempted |
| `no-webaudio` | browser lacks `AudioContext` |
| `init-failed` | `createMediaElementSource` threw |
| `cors-blocked` | analyser returned zeros for >2 s |
| `ok` | live frequency data flowing → canvas shown, bars hidden |
**Files changed:** `radio.php` (version), `inc/admin-page.php` + `inc/dashboard-widget.php` (added `.radio-player__indicator` with `.radio-player__bars` + ``, plus `crossorigin="anonymous"` on the ``), `assets/css/radio.css` (indicator container, bars + `radio-bars-dance` keyframes, canvas size), `assets/js/radio.js` (`tryVisualizer` / `startVizLoop` / `stopVizLoop`, `play/pause/error` handlers toggle `is-playing` class and drive the loop), `inc/about.php` (history entry).
---
## [0.3.2] — 2026-05-29 — Play-button glyph baseline fix
The dashicon used for the play/pause icon was rendering visibly below the button text baseline — the dashicon font sits the glyph low inside its own box, and even with `inline-flex` centering the result looked like the symbol was on a separate row from the word "Play".
### Fixed
- **Play/pause icon now sits on the text baseline.** Swapped the dashicon (`dashicons-controls-play` / `dashicons-controls-pause`) for a plain Unicode glyph (▶ / ‖) which renders on the text baseline like any other character.
- Flex container changed from `align-items: center` to `align-items: baseline` for the same reason.
- `font-variant-emoji: text` set so platforms that might otherwise pick up a colour-emoji variant for ▶ keep it as monochrome text.
**Files changed:** `radio.php` (version), `inc/admin-page.php` + `inc/dashboard-widget.php` (dashicon span → glyph span), `assets/css/radio.css` (drop the dashicon-sizing rule, add `.radio-player__play-glyph` styling, change play-button flex alignment to baseline), `assets/js/radio.js` (`setPlayIcon` now swaps glyph `textContent` instead of dashicon `className`), `inc/about.php` (history entry).
---
## [0.3.1] — 2026-05-29 — My Radio layout polish + drop dark-auto
Quick patch after a real-screen review of the My Radio admin page. v0.3.0 added a lot of features but stretched the player to fill the full admin width and — embarrassingly — introduced a real contrast bug via the dark-auto CSS.
### Fixed
- **Dropped `@media (prefers-color-scheme: dark)` for `theme=auto`.** WordPress admin has no native dark mode, so when the OS was dark, our dark text rendered on the still-white WP postbox = unreadable. `auto` now behaves as light (matching WP's actual scheme); `dark` is still available as an explicit choice.
- **`theme=dark` now actually reads.** The player surface goes dark (`#1d2327` background + subtle border + padding) so the light text has somewhere to sit, instead of fighting the white WP postbox.
- **Player no longer stretches edge-to-edge.** `.radio-wrap { max-width: 880px; }` keeps the player a focused settings-page card.
- **Intro paragraph one-line on normal widths.** Removed the `max-width: 720px` cap that was forcing the wrap.
- **Volume slider no longer dominates the row.** Fixed width 220px (`flex: 0 0 auto`) — the percent label now sits next to the slider instead of pinned to the far right edge.
- **Station dropdown** capped at 360px (was full-width) — typical WP form-control width.
- **Play button icon** shrunk from 18px to 14px so the ▶ glyph sits on the button-text baseline instead of looking like a separate row.
**Files changed:** `radio.php` (version), `assets/css/radio.css` (wrap width, intro, play icon, volume, station-select, dark-auto block deleted, dark-surface added), `inc/about.php` (history entry).
---
## [0.3.0] — 2026-05-29 — Dark theme + mute + media keys + current-track display
A polish pass that closes the gaps surfaced by a UI review. Two categories: **2nd-look fixes** (things the previous release implied but didn't actually deliver) and **nice-to-haves** (small upgrades that lift the admin-player feel).
### Added — features
- **Mute toggle on the speaker icon.** The icon next to the volume slider is now a button. Click to mute (icon flips to `dashicons-controls-volumeoff`, tinted red); click again to restore the prior volume. Remembers volume across mute/unmute via `data-prev-volume`.
- **MediaSession API integration.** OS media keys (F8/F9 on Mac, Bluetooth headphone buttons, lock-screen widget on supported platforms) now play/pause the radio. The currently-playing station name, "SomaFM" as artist, and the genre as album are exposed as `MediaMetadata` so they show on the OS overlays.
- **Current-track display.** Polls `https://somafm.com/songs/{code}.json` every 30 seconds **while playing only** and shows the track as `♪ Title — Artist` under the station description. Best-effort: silently hidden if the endpoint is unreachable / CORS-blocked, so the plugin keeps working regardless.
### Fixed — 2nd-look
- **Dark theme is now actually wired through.** v0.2.0 saved the Theme dropdown (auto / light / dark) but had no CSS to render anything other than light. v0.3.0 adds `admin_body_class` filter → `radio-theme-{auto,light,dark}` body class → corresponding dark-palette CSS for the player + about-cards. `auto` follows the OS via `prefers-color-scheme: dark`.
- **Settings-page volume slider** no longer uses an inline `oninput=""` handler — the listener moved into `assets/js/radio.js` (`bindSettingsSlider`). Cleaner under strict-CSP environments.
- **Save errors are surfaced.** AJAX state-save failures were previously swallowed silently — the local UI updated but the user had no signal if the server dropped the request. The plugin now shows a brief notice ("Preferences not saved — check your connection.") in the player's error slot for 3.5 s, then auto-clears.
- **Hardcoded Gitea URL** in the About page replaced with a `RADIO_GITEA_URL` constant defined in `radio.php`. One place to update if the repo ever moves.
- **Genre badge layout fix.** Was using `margin-left: auto` inside a wrap-enabled flex row, which caused it to land on its own line on narrow widget widths. Now styled as a small inline pill (rounded `rgba(0,0,0,0.06)` background) that flows naturally next to the station name.
### Other
- Plugin version bumped to **0.3.0**.
- New localized strings: `mute`, `unmute`, `saveError` (for the JS-driven UI).
- The mute button has a visible focus ring (`outline: 2px solid var(--wp-admin-theme-color)`) for keyboard navigation.
- Volume slider input now also exits mute state (sets `audio.muted = false` on drag), so dragging the slider always overrides a prior mute click.
**Files changed:** `radio.php` (version, constant, strings, `admin_body_class` filter), `inc/about.php` (constant for changelog URL), `inc/settings.php` (removed inline `oninput`), `inc/admin-page.php` + `inc/dashboard-widget.php` (speaker icon → mute button, added track slot), `assets/css/radio.css` (genre badge pill, mute button, track display, dark-theme rules incl. `prefers-color-scheme` for `auto`), `assets/js/radio.js` (full rewrite incl. `bindMute`, `bindSettingsSlider`, `startTrackPolling`/`stopTrackPolling`, `updateMediaSession`, save-error surfacing).
---
## [0.2.0] — 2026-05-26
### Changed — UI rebuilt to WordPress admin standards
v0.1.0 worked but looked like a third-party React widget sitting on top of WordPress rather than part of it. v0.2.0 rebuilds the player UI to use WP-native patterns end-to-end.
- **Main page** now uses the standard **`.postbox` container** (gray header bar + `.inside` body) instead of a custom rounded-shadow card.
- **Play button** is now a standard **`.button .button-primary`** with both icon AND text label (Play / Pause), matching every other admin button. Replaces the giant blue circular icon-only button.
- **Now Playing** uses left-aligned default body text with `.description` muted-gray for the station tagline. Replaces the centered large-typography card.
- **Genre badge** moved to a small `.radio-player__station-genre` text label aligned right of the station name. Replaces the custom pill.
- **Volume slider** now uses `accent-color: var(--wp-admin-theme-color)` — adapts to whichever admin colour scheme the user has chosen (Default / Light / Modern / Blue / Coffee / Ectoplasm / Midnight / Ocean / Sunrise).
- **All link colours** likewise adapt to the user's admin theme via `var(--wp-admin-theme-color, #2271b1)`.
- **Dashboard widget** content sits bare inside its `.inside` — WordPress already wraps it in a postbox. v0.1.0 was rendering a card inside a card.
- **About page** cards now use postbox-style gray header bars + WP-standard 1px border + subtle shadow. Replaces the custom rounded grid.
- **Credit footer** uses `.description` class, smaller and more native.
### Net effect
The plugin feels like part of WordPress now, not bolted onto it. Picks up your admin colour scheme automatically. Closer to WP.org submission criteria — they look for native-styled plugins during plugin review.
### Not changed
- Functionality identical to v0.1.0 — same 44 stations, same audio path, same user_meta persistence, same updater, same AJAX endpoint.
- No behaviour change for end users; this is purely visual.
- About page version-history card promotes v0.2.0 to "latest", demotes v0.1.0.
---
## [0.1.0] — 2026-05-26
**Radio is born.** First release of a new standalone WordPress plugin extracted-and-rebuilt from the radio feature that lives inside RangerPlex. Radio stands on its own as a focused, friendly companion plugin for the WordPress dashboard — a tab of background music while you work.
### Added — Phase A complete (player exists)
- **Dashboard widget** at WP Admin → Dashboard showing a compact mini-player with play/pause, station select grouped by genre, and a volume slider.
- **Dedicated admin page** at WP Admin → Radio → My Radio showing the same controls in a larger format with the station's genre badge and description.
- **44 SomaFM stations** across **10 genres** (Ambient, Electronic, Lounge, Rock, Metal, Jazz, World, Reggae, Holiday, Specials). Stream URLs use SomaFM's public 128kbps MP3 endpoints — no proxy server required.
- **Per-user state storage** via `user_meta` (key: `radio_state`). Each WordPress admin remembers their own station choice, volume, and theme preference.
- **Settings page** at WP Admin → Radio → Settings with default station, default volume, theme (auto/light/dark), and an opt-out for the dashboard widget. Updates panel shown to admins with `manage_options`.
- **About page** at WP Admin → Radio → About with plain-language explanation of what the plugin does, who it's for, version history, and credits to SomaFM.
- **Self-hosted update checker** wired up to the Gitea repo (`ranger/a-radio`) from commit 1. Polls `/api/v1/repos/ranger/a-radio/releases/latest` with a `/tags?limit=1` fallback. 12h success cache, 1h negative cache.
- **AJAX endpoint** `radio_save_state` for persisting station/volume changes without a page reload. Nonce-protected, capability-checked.
- **Custom admin-menu icon** (`dashicons-format-audio`).
- **Direct HTML5 `` playback** — no Node proxy, no PHP stream-passthrough, no server-side resource cost per listener. SomaFM's CORS headers make this work out of the box in modern browsers.
### Architecture (locked from day one)
- **Single-word brand name `Radio`** — no "WP" prefix, no marketplace trademark hurdle.
- **Public GPL v2+ Gitea repo** at `ranger/a-radio` on `git.davidtkeane.com`.
- **Per-user state in `user_meta`** under key `radio_state`.
- **Vanilla JS only** — no React, no build step, no bundler. ~200 lines of JS controlling all interactions.
- **CSS-only animations, all assets local** — bundle stays sub-100KB.
- **Single H1 per admin page**, no nested toggle boxes — Tier-1 discipline carried forward from the Logbook + Buddy lineage.
- **Sanitize on input, escape on output** throughout. Every AJAX endpoint nonce-protected and capability-checked.
### Compliance
- Station list and stream URLs are SomaFM's public, freely-published endpoints. Their terms allow redistribution with attribution.
- "Powered by SomaFM" credit displayed in both player surfaces, linking to somafm.com.
- The About page invites users to donate to SomaFM directly.
### Not in this release (planned)
- Phase B — Settings polish + README for WP.org submission + retry logic for transient stream errors.
- Phase C — Now-playing metadata via SomaFM's per-station song-history endpoint.
- Phase D — `[ranger_radio]` shortcode so the player can be embedded in posts/pages.
- Phase E — Favorites system.
- Phase F — Multi-provider (Radio Paradise, NTS Radio, KEXP, BBC) with a provider abstraction.