A third-party AI-driven naming check flagged `a-radio` as too generic for the wp.org Plugin Directory (single common functional word, no distinguishing prefix). The verdict was advisory but defensible — short generic slugs are increasingly rejected as the directory grows past 60K plugins. Fixing it preemptively is cheaper than facing a rejection at submission time. The new slug `rangerhq-radio` matches the public display name "RangerHQ Radio" (unchanged) and lines up with the rest of the RangerHQ plugin family: `rangerhq-spatial`, `rangerhq-glyph`, now `rangerhq-radio`. Changes (packaging only — no player behaviour change): * Text Domain `a-radio` → `rangerhq-radio` across all 125 i18n call sites via in-place sed (esc_html__, _e, __, esc_attr_e and their friends). PHP lint clean post-rename. * `Text Domain:` plugin header in radio.php line 15 → `rangerhq-radio`. * `RADIO_GITEA_URL` constant value → new Gitea repo URL. * README.md install link → new repo URL. * readme.txt FAQ Gitea link → new repo URL. * readme.txt Stable tag → 0.7.5. * inc/about.php — v0.7.5 in "latest" slot; v0.7.4 demoted. Unchanged deliberately (would have been pure churn): * Plugin Name header "RangerHQ Radio" — already correct. * Plugin URI. * Internal constants `RADIO_*` — don't have to match slug. * User-meta keys `radio_state` / `radio_history` / `radio_favourites` — renaming would orphan every existing user's settings on upgrade. * HTML `data-radio-*` attributes — JS controller's element selectors in radio.js, not slug-related. * CSS class names `radio-player`, `radio-about-*` — internal scoping. * Main plugin file name `radio.php`. Migration: existing Gitea-installed copies need their folder renamed on disk (a-radio → rangerhq-radio) + reactivation in WP admin. No data loss because all user-facing state lives in user_meta under unchanged keys. This commit is the file content for v0.7.5; the annotated tag and push will follow the Gitea repo rename (ranger/a-radio → ranger/rangerhq-radio) so the new tag lives in the new URL space from the start.
46 KiB
Changelog
All notable changes to Radio are documented here. Format: Keep a Changelog 1.1.0 — versioning: SemVer.
[Unreleased]
[0.7.5] — 2026-05-30 — WordPress.org slug rename: a-radio → rangerhq-radio
A third-party AI-driven naming-check tool (run via Plugin Check's Claude AI connector against the v0.7.4 build) flagged the slug as too generic for the wp.org Plugin Directory:
The display name "a-radio" is too generic. The term "radio" is a broadly used, common functional word, and the single-letter prefix "a-" does not add meaningful distinctiveness or describe what the plugin actually does. With 60,000+ plugins in the directory, a name this short and generic makes it hard for users to identify the plugin and distinguish it from others.
The verdict was advisory (not the official wp.org reviewer) but defensible — single-word generic slugs are increasingly rejected by human reviewers as the directory grows. Better to fix it preemptively than face a rejection at submission time.
The new slug rangerhq-radio matches the public display name "RangerHQ Radio" (which is unchanged) and lines up with the rest of the RangerHQ plugin family: rangerhq-spatial, rangerhq-glyph, now rangerhq-radio. Consistent prefix, distinctive enough for the directory, descriptive of what the plugin does.
Changed
- Plugin slug
a-radio→rangerhq-radio. The slug is the plugin's folder name + the URL fragment atwordpress.org/plugins/<slug>/. Folder renamed on disk; Gitea repository moved fromranger/a-radiotoranger/rangerhq-radio(history + tags + issues preserved by Gitea's rename). - Text Domain
a-radio→rangerhq-radioto match the slug (wp.org best practice + PCPtextdomain_mismatchrequirement). All 125 i18n call sites updated via in-placesedacross every*.phpfile:__(),_e(),esc_html__(),esc_html_e(),esc_attr__(),esc_attr_e(), and the translator-comment arguments. PHP lint clean post-rename. Text Domain:plugin header inradio.phpline 15 updated to match.RADIO_GITEA_URLconstant value updated fromhttps://git.davidtkeane.com/ranger/a-radiotohttps://git.davidtkeane.com/ranger/rangerhq-radioto follow the Gitea repo rename. The constant name stays the same (internal naming; no churn).- Install URL in root
README.md— repo link in the install step updated. - Gitea issue link in
readme.txtFAQ — updated to the new repo URL. readme.txtStable tag →0.7.5.inc/about.phpVersion history — v0.7.5 rotated into the "latest" slot; v0.7.4 demoted to a one-liner in the earlier-releases list.
Unchanged (deliberately — would have been pure churn)
- Plugin Name header (
RangerHQ Radio) — already correct, distinctive, and brand-aligned. - Plugin URI (
https://icanhelp.ie/radio). - Internal PHP constants (
RADIO_VERSION,RADIO_FILE,RADIO_PATH,RADIO_URL,RADIO_BASENAME,RADIO_GITEA_URL,RADIO_SUPPORT_URL,RADIO_META_KEY). Constants don't have to match the slug; renaming them would have rippled through every PHP file for zero functional benefit. - User-meta keys (
radio_state,radio_history,radio_favourites). Renaming would have orphaned every existing user's settings + history + favourites on upgrade. - HTML
data-radio-*attributes in the player markup (data-radio-play,data-radio-viz,data-radio-station, etc.) — these are the JS controller's element selectors inradio.js, not slug-related. They share the word "radio" by coincidence; renaming would have required correspondingradio.jschanges for zero benefit. - CSS class names (
radio-player,radio-player__*,radio-about-*, etc.) — same reasoning. Internal scoping. - Main plugin file name (
radio.php). WordPress doesn't require the main file to be named after the slug; it just has to exist in the slug folder. Many plugins ship with<slug>/<short-name>.php. Keepingradio.phpsaved a rename + a corresponding update to every git reference.
Migration
- wp.org installs (once accepted): no migration — wp.org will accept the plugin under the new slug from day one.
- Existing Gitea-installed copies (the local dev install at
/Users/ranger/Local Sites/plugin-test/.../a-radio/was renamed in-place to.../rangerhq-radio/as part of this release; M5 or other Gitea installs would need the same): deactivate the plugin in WP admin (so WP releases its hold ona-radio/radio.php), rename the folder on disk, reactivate via the new entry atrangerhq-radio/radio.php. WordPress'sactive_pluginsoption inwp_optionsis keyed on the basename (folder/file.php), so a folder rename appears to WP as an uninstall + a new install — but no data is lost because all user-facing state lives inuser_metaunder unchanged keys (radio_state/radio_history/radio_favourites). - Tags + git history: preserved across the Gitea repo rename. The annotated tag for v0.7.5 was pushed AFTER the Gitea repo rename so it lives in the new URL space from the start.
Why the slug matters even though "RangerHQ Radio" is fine
WordPress.org reviewers evaluate two strings independently — the display name (Plugin Name header, what users see in their admin) and the slug (folder name, what appears in the URL at wordpress.org/plugins/<slug>/). They have to be distinct enough that:
- Users can find the plugin via wp.org search.
- The URL is recognisable and not easily confused with other plugins.
- The slug doesn't collide with an already-registered plugin name.
"RangerHQ Radio" was always fine as the display name. The slug a-radio was the weak link — short, generic, with a non-descriptive prefix. rangerhq-radio is none of those things.
[0.7.4] — 2026-05-30 — WordPress.org submission cleanup (updater out, LICENSE in)
Walks back the Update URI guard pattern from v0.7.3 and replaces it with the simpler, actually-correct answer: just remove the custom updater entirely. The WordPress.org Plugin Check tool (PCP) raised plugin_updater_detected on the v0.7.3 build with the message "Including An Update Checker / Changing Updates functionality. Plugin Updater detected. Use of the Update URI header is not allowed in plugins hosted on WordPress.org." PCP scans the source as-shipped, not as-distributed, so the build-time sed strip we relied on in v0.7.3 never had a chance to run before the scan — and the scanner flagged both the header and the inc/updater.php file itself. Two options remained: (a) keep dancing around PCP with branches and build scripts, or (b) accept that WordPress.org is the canonical update channel once a plugin is hosted there and drop the custom system. We went with (b).
This release also closes the per-file GPL declaration loop by adding a top-level LICENSE file with the full GPL v2 text and explicit GPL header blocks to the CSS and JS assets — every shipped file now declares its license unambiguously.
Removed
inc/updater.php— the self-hosted Gitea updater. Deleted from the working tree (still recoverable from git history at tagv0.7.3if ever needed for a non-wp.org distribution). With the file gone, PCP'splugin_updater_detectedcheck is silent.Update URI:header inradio.php. PCP forbids the header in wp.org-hosted plugins, even when its value would legitimately defer updates to wp.org. (The v0.7.3 NOTE block in the file header documenting the strip-on-package pattern was removed along with it.)require_once RADIO_PATH . 'inc/updater.php';line inradio.php. The file no longer exists, so no require is needed.- Updates panel render block +
function_exists()guard ininc/settings.php. The panel function no longer exists in any build, so the call site is gone. Settings page now ends cleanly at the Save Changes button. - "Self-hosted update checker against the Gitea repo" bullet in the top-level
README.md. Replaced with "Updates via WordPress.org (the canonical channel once published)". - "Self-hosted updater" bullet in the
readme.txtPrivacy section. Replaced with a positive statement that updates come from WordPress.org through the normal core update process.
Added
LICENSEfile at the plugin root — verbatim canonical GPL v2 text fromhttps://www.gnu.org/licenses/gpl-2.0.txt, 338 lines. The plugin header has always declared GPL v2+ but wp.org reviewers like to see the full text shipped alongside.- GPL header block in
assets/css/radio.css(Copyright, GPL v2+ grant, pointer to LICENSE) so the CSS file's licensing is explicit at the file level, not inferred from the plugin header. - GPL header block in
assets/js/radio.js— same treatment as the CSS. The original/** Radio — vanilla JS audio controller. */opening is preserved verbatim as the "module overview" section below the license header. - GPL header block in
radio.php's docblock — Copyright line + GPL v2+ grant + LICENSE pointer added below the@packagetag. Sits alongside the existingLicense:/License URI:header fields rather than replacing them.
Changed
readme.txtStable tag →0.7.4.inc/about.phpVersion history — v0.7.4 rotated into the "latest" slot; v0.7.3 demoted to a one-liner in the earlier-releases list with a note explaining the walkback.RADIO_GITEA_URLconstant is kept inradio.php. It's no longer used by an updater, butinc/about.phpstill uses it to render the "View the full CHANGELOG.md →" outbound link on the About page. Linking to a self-hosted source repo as a documentation reference is not the same as running a custom updater — that's fine in wp.org-hosted plugins. (WordPress.org allows links to external source repos — the prohibition is on the update mechanism, not on referring to where the source lives.)
Migration
- Existing wp.org installs (post-acceptance): WordPress core handles updates automatically. Nothing to do.
- Existing Gitea-installed copies of v0.7.3 or earlier (e.g. M5): the
inc/updater.phpfile on disk will become orphaned after this update lands on them — it's no longer required fromradio.php, so it just sits there as a dormant file until the next reinstall. Recommended path for those installs: once a-radio is accepted onto WordPress.org, uninstall the Gitea-sourced copy and reinstall from WordPress.org. Future updates then flow through wp.org. Until acceptance, those installs simply stop receiving updates (the v0.7.3 updater code is gone in v0.7.4, so there's nothing to advertise newer versions back to them). - No data loss in any case: station + volume + theme + history + favourites all live in
user_metaand survive uninstall/reinstall cycles.
Why this is the right answer (and v0.7.3 was clever but wrong)
v0.7.3's Update URI guard pattern reasoned correctly about WordPress core's behaviour — the Update URI header genuinely does opt a plugin out of wp.org's update channel, and the runtime guard genuinely would short-circuit the updater on stripped builds. But the whole scheme assumed the reviewer would see the stripped build. The Plugin Check tool runs against the source artifact — the same source that's in the public repository, that future contributors clone, that automated audits scan. From PCP's point of view, the plugin was "shipping" an updater regardless of what the build script did with it later. The simpler answer (just delete the updater for the wp.org branch) sidesteps that entire class of "the static scanner doesn't know about your build pipeline" problem.
[0.7.3] — 2026-05-30 — WordPress.org guideline 8 compliance + Privacy section
WordPress.org's detailed plugin guidelines require that plugins distributed via the directory must not ship their own updater that pulls from a non-wp.org server (guideline 8). RangerHQ Radio's self-hosted Gitea updater predates the wp.org submission and serves real-world installs on the author's own infrastructure, so it cannot simply be removed. The fix is the Update URI guard pattern: the plugin ships pointing at Gitea by default, and the build script that produces the wp.org submission zip strips the Update URI: header line. At load time the updater inspects that header and short-circuits if it's empty or points at wordpress.org.
Net effect: one source tree, two build artifacts, both compliant.
Added
Update URI:header inradio.phppointing athttps://git.davidtkeane.com/ranger/a-radio. WordPress core uses this header to decide whether wp.org should poll this plugin for updates — a non-wordpress.org URI opts the plugin out of wp.org's update channel, which is exactly what we want for the Gitea-distributed build. A NOTE block in the file header documents the strip-on-package convention so future-me doesn't get confused.radio_should_skip_custom_updater()helper at the top ofinc/updater.php. Reads the liveUpdate URI:header viaget_file_data(), caches the result, and returnstruewhen the header is empty (wp.org default) or containswordpress.org. Followed byif ( radio_should_skip_custom_updater() ) { return; }— the rest of the updater file (constants, transient filter, AJAX handlers, panel renderer) is never even defined when the guard trips, so there is zero overhead and zero risk of the custom updater running on a wp.org install.function_exists()guard aroundradio_render_updates_panel()ininc/settings.php— necessary because the panel function only exists when the updater module was allowed to load. With the guard, the Settings page silently omits the panel on wp.org installs and renders it normally on Gitea installs.- Dedicated
== Privacy ==section inreadme.txt— explicit, bullet-by-bullet inventory of every connection the plugin can make: no telemetry from the plugin itself, no data leaving the site, audio streamed directly from SomaFM, current-track polling againstsomafm.com/songs/{station}.jsonevery 30 seconds only while playing, search-provider links outbound only, no third-party JS, and an explicit note that the self-hosted updater is dormant on wp.org installs. Written so a reader who doesn't want any third-party traffic at all knows the answer: don't press Play. - SomaFM Terms of Use link (
https://somafm.com/legal/) prominently in the readme's third-party-service note. Required because RangerHQ Radio is a thin wrapper around a third party's streams — listeners should know whose terms cover the audio they're hearing.
Changed
readme.txtStable tag →0.7.3.readme.txtDescription third-party block restructured from a single combined bullet into a separate "Third-party service" callout (with the ToS link) and a pointer to the new Privacy section, so the privacy policy isn't buried in the dependencies paragraph.inc/about.phpVersion history — v0.7.3 rotated into the "latest" slot (full description); v0.7.2 demoted to a one-liner in the earlier-releases list.
Why
WordPress.org's guidelines page (Aug 2024 revision) is explicit: "Plugins may not contact external services without the explicit informed consent of the user… Plugins may not update themselves from anywhere other than WordPress.org once they're hosted there." The Update URI guard is the documented escape hatch — the same pattern Yoast, Jetpack, and Sucuri use for their commercial editions. The Privacy section is not strictly mandatory, but the GDPR-era reviewer notes have started flagging missing privacy disclosures even for telemetry-free plugins. Easier to ship with one than to play the comments game after submission.
Migration
None. Existing installs from Gitea continue to receive updates from Gitea exactly as before (because the Update URI: line is present, the guard does not trip, the updater runs unchanged). The wp.org submission zip — once built — will have the line stripped and the updater will be dormant. No user-visible behaviour change in either distribution.
[0.7.2] — 2026-05-30 — Screenshots + correct wp.org contributor handle
Two small but real submission-prep moves:
Added
- Five screenshots at the plugin root, named per WordPress.org convention (
screenshot-1.pngthroughscreenshot-5.png). Sizes 1087–1422 wide. Order matches the readme:- Dashboard widget
- Settings page
- History page (star + four search-provider links)
- Pop-out mini-player window
- About page
Changed
Contributors:inreadme.txtupdated fromdavidtkeane(placeholder) toir240474— actual wp.org username confirmed (profile: https://profiles.wordpress.org/ir240474/).Stable tag:bumped to0.7.2.- Screenshots section in
readme.txtrewritten to match the five actual screenshots provided.
Notes
- Source images came from an
images/working folder (David's working location); they've been moved/renamed to plugin root for the wp.org screenshot convention, and the working folder removed. - After this release the only PCP residue is the
.gitignorehidden-file warning, which is unavoidable on the dev install and won't be in the submission zip.
Files changed: radio.php (version), readme.txt (Contributors, Stable Tag, Screenshots section, new 0.7.2 changelog + upgrade notice), CHANGELOG.md, inc/about.php (rotate v0.7.2 → latest), 5 new files screenshot-1.png through screenshot-5.png at plugin root, images/ directory removed.
[0.7.1] — 2026-05-30 — Plugin Check follow-up: tested-up-to + .DS_Store re-cleanup
A re-run of PCP after v0.7.0 dropped the issue count from 169 → 4. This release closes the only real one of the four:
Fixed
Tested up tobumped: 6.7 → 7.0 inreadme.txt(PCPoutdated_tested_upto_header). WordPress 7.0 is current; the previous header would have been flagged as out-of-date at submission review.- Stable Tag bumped to 0.7.1 in
readme.txt.
Removed (again)
.DS_Storefiles (root +assets/). macOS Finder regenerated them between PCP runs; they will not be present in the actual submission zip..gitignorealready covers them.
Known PCP residue (not addressable in source)
- The
.gitignorefile itself triggers ahidden_filesWARNING (sev 8) on PCP because it's a dot-file. Keeping.gitignoreis needed for git; it will be excluded from the submission zip. Acceptable per WP.org packaging conventions.
Files changed: radio.php (version), readme.txt (Tested-up-to + Stable Tag + new 0.7.1 changelog + upgrade notice), CHANGELOG.md, .DS_Store files deleted.
[0.7.0] — 2026-05-30 — WordPress.org submission prep (full Plugin Check clean)
Ran the official WordPress.org Plugin Check (PCP) against v0.6.3 — surfaced 169 issues. This release closes all of them so the plugin is submission-ready for the WordPress.org plugin directory.
Changed — branding
- Plugin Name renamed: "Radio" → "RangerHQ Radio" to (a) remove the trademarked term "SomaFM" from the plugin name surface (PCP
trademarked_termwarning) and (b) line up with the RangerHQ plugin family. SomaFM is still credited in the Description and on the About page as the data source. Plugin folder/slug staysa-radio— no install path changes; existing user_meta keys (radio_state/radio_history/radio_favourites) untouched. Text Domainheader renamed:radio→a-radio(matched the slug for the first time; PCPtextdomain_mismatch).Requires at leastbumped:5.0→5.3(matcheswp_date()usage ininc/history.php; PCPwp_function_not_compatible_with_requires_wp).- File docstring header dropped "SomaFM" from the prominent first line.
Changed — code (mass-mechanical)
- 134 i18n call sites rewritten from
'radio'text domain →'a-radio'acrossradio.php,inc/admin-page.php,inc/dashboard-widget.php,inc/settings.php,inc/about.php,inc/history.php,inc/updater.php. Single sed pass on the unique pattern, 'radio' )(the 6 menu-slug'radio'references inadd_menu_page/add_submenu_pagewere left alone — they're the URL slug, not the text domain).
Fixed — security
- 8 ×
MissingUnslash+ 8 ×InputNotSanitizedin the v0.5.0 history endpoints (radio_ajax_log_track,radio_ajax_toggle_favourite). All four$_POST['artist|title|station|station_id']access points are now wrappedsanitize_text_field( wp_unslash( $_POST['…'] ) )(orsanitize_key()forstation_id) at the access point. The downstreamradio_sanitize_entry()helper still re-sanitizes as belt+braces.
Added — translator comments
- 6 ×
printf/sprintfcalls with placeholders now carry/* translators: ... */comments: pop-out window title, updater status messages (HTTP error / available / up-to-date / download), history page "%s ago" relative time.
Refactored — pop-out window
- Inline
<link>stylesheets, inline<style>block, and<script>tag inradio_render_popout_page()replaced withwp_enqueue_style()+wp_enqueue_script()+wp_localize_script()registered before the HTML output, thenwp_print_styles()in<head>andwp_print_footer_scripts()at end of<body>. - Popup-specific CSS (header, close button, wrap, layout overrides, popup dark theme) moved out of the inline
<style>and intoassets/css/radio.cssunderbody.radio-popoutscope so it only fires inside the popup.
Removed
.DS_Storefiles (root +assets/) — stray macOS Finder artefacts; PCPhidden_files..gitignorealready covers them.
Added — distribution
readme.txtin proper WordPress.org format: Plugin headers, Contributors, Donate link, Tags, Requires-at-least, Tested-up-to, Stable Tag, Requires-PHP, License, License URI, short description (≤150 chars), Description, Installation, FAQ, Screenshots, Changelog, Upgrade Notice.
Compat notes
- No behaviour change for existing users. Per-user state (
radio_state/radio_history/radio_favourites) is preserved across the upgrade — no migration needed. - The displayed Plugin Name in Plugins → Installed changes from "Radio" to "RangerHQ Radio" — that's the only visible difference on update.
Files changed: radio.php (header + 6 translator comments + 8 $_POST hardenings + popup enqueue refactor), inc/admin-page.php / inc/dashboard-widget.php / inc/settings.php / inc/about.php / inc/history.php / inc/updater.php (textdomain mass-fix + translator comments where applicable + about-page version rotation), assets/css/radio.css (popup styles moved in under body.radio-popout), new readme.txt, removed .DS_Store, assets/.DS_Store.
[0.6.3] — 2026-05-30 — Discreet "buy me a coffee" support link
Added
- New
RADIO_SUPPORT_URLconstant inradio.php(defaulthttps://buymeacoffee.com/davidtkeane). Wrapped inif ( ! defined(...) )so it's override-able fromwp-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_URLis 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 (
#2c3338background,#c3c4c7text) so it stays readable whentheme=dark.
Files changed: radio.php (version), inc/settings.php (<span class="radio-version-badge">v…</span> 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.mdon 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 <audio> 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 outbutton 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<!DOCTYPE>, 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
<audio>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.jsreads it from the localized config and auto-callsaudio.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 theradio-theme-darkclass 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 callswindow.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_stateAJAX 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_trackexactly 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 newlogTrackIfNewhelper that POSTs it towp_ajax_radio_log_track.- Dedup: client-side via
lastLoggedSigso 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 inradio_sanitize_entry.
Added — per-user storage
- Two new
user_metakeys, separate fromradio_stateso 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
@keyframesanimation 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-playingclass toggled on the.radio-playersurface from the existingplay/pause/erroraudio handlers.
Added — Web Audio frequency visualizer (progressive upgrade)
- On first
play,tryVisualizerbuilds anAudioContext+AnalyserNodechain on the<audio>element and starts drawing live frequency bars on a<canvas>next to "Now Playing." <audio>now carriescrossorigin="anonymous"so the Web Audio analyser can actually read the stream data (SomaFM serves the CORS headers).- Graceful fallback: if
AudioContextisn't available, orcreateMediaElementSourcethrows, or the analyser returns all-zeros for 2 s (CORS silently blocking), the visualizer state flips tocors-blocked/init-failed/no-webaudioand the CSS dancing bars remain — the plugin never loses its indicator. - Canvas is sized to its CSS box ×
devicePixelRatioso 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 + <canvas data-radio-viz>, plus crossorigin="anonymous" on the <audio>), 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: centertoalign-items: baselinefor the same reason. font-variant-emoji: textset 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)fortheme=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.autonow behaves as light (matching WP's actual scheme);darkis still available as an explicit choice. theme=darknow actually reads. The player surface goes dark (#1d2327background + 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: 720pxcap 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 viadata-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
MediaMetadataso they show on the OS overlays. - Current-track display. Polls
https://somafm.com/songs/{code}.jsonevery 30 seconds while playing only and shows the track as♪ Title — Artistunder 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_classfilter →radio-theme-{auto,light,dark}body class → corresponding dark-palette CSS for the player + about-cards.autofollows the OS viaprefers-color-scheme: dark. - Settings-page volume slider no longer uses an inline
oninput=""handler — the listener moved intoassets/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_URLconstant defined inradio.php. One place to update if the repo ever moves. - Genre badge layout fix. Was using
margin-left: autoinside 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 (roundedrgba(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 = falseon 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
.postboxcontainer (gray header bar +.insidebody) instead of a custom rounded-shadow card. - Play button is now a standard
.button .button-primarywith 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
.descriptionmuted-gray for the station tagline. Replaces the centered large-typography card. - Genre badge moved to a small
.radio-player__station-genretext 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
.descriptionclass, 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/latestwith a/tags?limit=1fallback. 12h success cache, 1h negative cache. - AJAX endpoint
radio_save_statefor persisting station/volume changes without a page reload. Nonce-protected, capability-checked. - Custom admin-menu icon (
dashicons-format-audio). - Direct HTML5
<audio>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-radioongit.davidtkeane.com. - Per-user state in
user_metaunder keyradio_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.