Files
rangerhq-radio/CHANGELOG.md
T
ranger 0dc3a220d9 feat(0.6.0): pop-out mini-player — continuous background play across admin nav
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.

Pop-out window
  - Small "↗ Pop out" button beside the Play button on both the main
    Radio page and the Dashboard widget.
  - Opens a 380×560 standalone window via window.open() with a window
    name of `radio_popout` so a second click re-focuses the existing
    popup rather than spawning a new one.
  - Popup renders at admin-post.php?action=radio_popout&play=1 — a new
    admin_post_radio_popout hook outputs a full standalone HTML page
    (custom DOCTYPE / head / body, no WP admin chrome).
  - Theme follows radio_state['theme'] via body class radio-theme-{...}.
  - Includes the full player (now-playing block with dancing bars +
    Web Audio visualizer, play, mute, volume, station dropdown, error
    slot). Close button calls window.close().

Auto-resume + single-stream invariant
  - cfg.autoPlay is true when the popup URL carries &play=1. radio.js
    auto-calls audio.play() 200 ms after init. Same-origin user-gesture
    popups are exempt from autoplay-blocking on every modern browser.
  - On pop-out click, every other audio surface in the parent tab is
    paused so there is only ever one stream playing.

State stays in sync
  - Popup uses the SAME radio_save_state AJAX + track-logging endpoint.
    Station / volume changes persist to user_meta; track history keeps
    accumulating from whichever surface is playing.

Edge cases
  - Inside the popup, cfg.popoutUrl is '' so bindPopOut hides any
    Pop-out button it finds (would be infinite otherwise).
  - Popup blocked by browser → clear alert tells the user to allow
    popups for this site.

Files
  - radio.php (version, popoutUrl in localized config, admin_post
    handler + full standalone HTML renderer)
  - inc/admin-page.php + inc/dashboard-widget.php (Pop-out button
    beside Play)
  - assets/css/radio.css (.radio-player__popout styling)
  - assets/js/radio.js (bindPopOut function; autoPlay branch in
    bindPlayer)
  - inc/about.php + CHANGELOG.md (history entries)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:03:21 +01:00

221 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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 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 `<!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.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** (~5080 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 `<audio>` element and starts drawing live frequency bars on a `<canvas>` next to "Now Playing."
- `<audio>` 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` + `<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: 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 `<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-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.