3 Commits

Author SHA1 Message Date
ranger 32a3040e39 feat(0.6.2): current version badge on Settings page
Small grey pill follows the "Radio — Settings" H1 — 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 so it stays readable when theme=dark.

Files: radio.php (version), inc/settings.php (<span> inside H1),
assets/css/radio.css (.radio-version-badge + dark override),
inc/about.php (rotate v0.6.2 into latest expanded slot, v0.6.1 into
earlier-releases list), CHANGELOG.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:30:14 +01:00
ranger 7a747b829b fix(0.6.1): About page restructure — balance the layout
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.

- Three short cards on top (What / Who / Credits) — equal-height,
  balanced row. Credits gets equal billing instead of being a fourth
  card buried under a wall of version notes.
- Version history is its own full-width card below. Only the latest
  release is shown in full; earlier releases collapse to one line
  each. 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 via
  the existing "View the full CHANGELOG.md →" link. Single source of
  truth.

Files: 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 (drop dead .radio-about-card--versions rules;
add .radio-about-versions + __latest + __earlier; dark-theme overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:25:12 +01:00
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
8 changed files with 386 additions and 68 deletions
+52
View File
@@ -9,6 +9,58 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versi
--- ---
## [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` (`<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.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 `<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 ## [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. 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.
+109 -23
View File
@@ -166,6 +166,16 @@
font-family: "Helvetica Neue", Arial, sans-serif; font-family: "Helvetica Neue", Arial, sans-serif;
} }
/* v0.6.0: Pop-out button — small secondary button beside Play. Uses WP
native .button styles; the ↗ glyph signals "opens in another window." */
.radio-player__popout {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.radio-player__popout span[aria-hidden] { font-size: 13px; line-height: 1; }
.radio-player__volume { .radio-player__volume {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -287,6 +297,22 @@
font-size: 13px; font-size: 13px;
} }
/* Small grey pill that follows the Settings page H1 — at-a-glance
confirmation of the version you are running (v0.6.2). */
.radio-version-badge {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
background: #e2e4e7;
color: #50575e;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
vertical-align: middle;
letter-spacing: 0.02em;
}
.radio-theme-dark .radio-version-badge { background: #2c3338; color: #c3c4c7; }
/* ────────────────────────────────────────────────────────────────── /* ──────────────────────────────────────────────────────────────────
* Dashboard widget — no nested card; bare content inside .inside * Dashboard widget — no nested card; bare content inside .inside
* (WP renders the widget as a postbox already; don't double up) * (WP renders the widget as a postbox already; don't double up)
@@ -349,27 +375,50 @@
padding-top: 0; padding-top: 0;
} }
.radio-about-card--versions ul { .radio-about-changelog-link {
list-style: none; display: inline-block;
padding: 12px; margin: 0 12px 12px;
margin: 0; font-size: 13px;
color: var(--wp-admin-theme-color, #2271b1);
text-decoration: none;
}
.radio-about-changelog-link:hover { text-decoration: underline; }
/* ──────────────────────────────────────────────────────────────────
* Version history — full-width card BELOW the top row of cards
* (v0.6.1). Latest release shown in full; earlier releases collapse
* to one line each so the card stays compact however many versions
* ship. Full prose for older versions lives in the CHANGELOG on Gitea.
* ─────────────────────────────────────────────────────────────── */
.radio-about-versions {
max-width: 1100px;
margin-top: 16px;
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
} }
.radio-about-card--versions li { .radio-about-versions h2 {
margin-bottom: 10px; margin: 0;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
background: #f6f7f7;
border-bottom: 1px solid #c3c4c7;
}
.radio-about-versions__latest {
padding: 12px 12px 8px;
}
.radio-about-versions__latest .ver {
font-weight: 600;
color: var(--wp-admin-theme-color, #2271b1);
font-size: 13px; font-size: 13px;
} }
.radio-about-card--versions li:last-child { .radio-about-versions__latest .latest {
margin-bottom: 0;
}
.radio-about-card--versions .ver {
font-weight: 600;
color: var(--wp-admin-theme-color, #2271b1);
}
.radio-about-card--versions .latest {
display: inline-block; display: inline-block;
margin-left: 6px; margin-left: 6px;
padding: 1px 7px; padding: 1px 7px;
@@ -381,16 +430,47 @@
vertical-align: middle; vertical-align: middle;
} }
.radio-about-changelog-link { .radio-about-versions__latest p {
display: inline-block; margin: 6px 0 0;
margin: 0 12px 12px;
font-size: 13px; font-size: 13px;
color: var(--wp-admin-theme-color, #2271b1); color: #1d2327;
text-decoration: none;
} }
.radio-about-changelog-link:hover { .radio-about-versions h3 {
text-decoration: underline; margin: 4px 0 0;
padding: 8px 12px 4px;
font-size: 11px;
font-weight: 600;
color: #646970;
text-transform: uppercase;
letter-spacing: 0.06em;
border-top: 1px solid #f0f0f1;
}
.radio-about-versions__earlier {
list-style: none;
margin: 0;
padding: 4px 12px 8px;
}
.radio-about-versions__earlier li {
padding: 4px 0;
font-size: 13px;
color: #50575e;
}
.radio-about-versions__earlier .ver {
display: inline-block;
min-width: 48px;
font-weight: 600;
color: var(--wp-admin-theme-color, #2271b1);
}
.radio-about-versions__earlier .ver-date {
display: inline-block;
min-width: 110px;
font-size: 11px;
color: #646970;
} }
/* ────────────────────────────────────────────────────────────────── /* ──────────────────────────────────────────────────────────────────
@@ -440,6 +520,12 @@
color: #f0f0f1; color: #f0f0f1;
border-bottom-color: #3c434a; border-bottom-color: #3c434a;
} }
.radio-theme-dark .radio-about-versions { background: #1d2327; border-color: #3c434a; color: #c3c4c7; }
.radio-theme-dark .radio-about-versions h2 { background: #2c3338; color: #f0f0f1; border-bottom-color: #3c434a; }
.radio-theme-dark .radio-about-versions h3 { color: #a7aaad; border-top-color: #3c434a; }
.radio-theme-dark .radio-about-versions__latest p { color: #c3c4c7; }
.radio-theme-dark .radio-about-versions__earlier li { color: #c3c4c7; }
.radio-theme-dark .radio-about-versions__earlier .ver-date { color: #a7aaad; }
.radio-theme-dark #radio_dashboard_widget .radio-player__credit { .radio-theme-dark #radio_dashboard_widget .radio-player__credit {
border-top-color: #3c434a; border-top-color: #3c434a;
} }
+38
View File
@@ -412,6 +412,44 @@
} }
bindMute(player, audio, volumeIn, volPctEl); bindMute(player, audio, volumeIn, volPctEl);
bindPopOut(player);
// v0.6.0: popout auto-resume — when opened with ?play=1, immediately
// pick up where the main tab left off. Same-origin user-gesture popups
// are exempt from autoplay blocking on every modern browser.
if (cfg.autoPlay) {
setTimeout(function () {
if (audio.paused) { audio.play().catch(function () { /* autoplay denied — user just clicks play */ }); }
}, 200);
}
}
/** v0.6.0: Pop-out window button. Opens a 380×560 standalone window
* with just the player chrome (no WP admin), via admin-post.php?
* action=radio_popout&play=1. The popup persists across main-tab
* navigation so background music doesn't cut when you move around
* the WP admin. Pauses every other audio surface in this tab so
* there's only one stream playing at a time. */
function bindPopOut(player) {
var btn = player.querySelector('[data-radio-popout]');
if (!btn) { return; }
if (!cfg.popoutUrl) { btn.style.display = 'none'; return; } // already in popout
btn.addEventListener('click', function () {
var w = window.open(
cfg.popoutUrl + '&play=1',
'radio_popout',
'width=380,height=560,resizable=yes,scrollbars=no,toolbar=no,location=no,menubar=no,status=no'
);
if (!w) {
window.alert('Pop-out blocked by the browser. Allow popups for this site, then try again.');
return;
}
w.focus();
// Pause every audio surface in this tab — the popup is the new player.
document.querySelectorAll('[data-radio-audio]').forEach(function (a) {
if (!a.paused) { a.pause(); }
});
});
} }
/** Keep all .radio-player surfaces on the same station. */ /** Keep all .radio-player surfaces on the same station. */
+39 -42
View File
@@ -2,9 +2,12 @@
/** /**
* Radio — About page (WP Admin → Radio → About). * Radio — About page (WP Admin → Radio → About).
* *
* Plain-language explanation of what the plugin does + version * Top row: three short cards (What / Who / Credits) — equal-height,
* history + link to the CHANGELOG.md on Gitea. Pattern mirrors * balanced layout. Below: a full-width Version history card with the
* a-buddy/inc/about.php. * latest release shown in full and earlier releases as a one-line
* summary list, capped by a link to the full CHANGELOG on Gitea.
*
* Pattern mirrors a-buddy/inc/about.php.
*/ */
if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! defined( 'ABSPATH' ) ) { exit; }
@@ -19,6 +22,7 @@ function radio_render_about_page() {
<div class="wrap radio-about-wrap"> <div class="wrap radio-about-wrap">
<h1><?php esc_html_e( 'About Radio', 'radio' ); ?></h1> <h1><?php esc_html_e( 'About Radio', 'radio' ); ?></h1>
<!-- ── Top row: three short cards (What / Who / Credits) ── -->
<div class="radio-about-grid"> <div class="radio-about-grid">
<div class="radio-about-card"> <div class="radio-about-card">
@@ -44,45 +48,6 @@ function radio_render_about_page() {
</p> </p>
</div> </div>
<div class="radio-about-card radio-about-card--versions">
<h2><?php esc_html_e( 'Version history', 'radio' ); ?></h2>
<ul>
<li>
<span class="ver">v0.5.0</span> &mdash; 29 May 2026 <span class="latest">latest</span><br>
<?php esc_html_e( 'Track history + favourites. Every track that scrolls past in the player is now quietly logged to your personal Radio → History page (capped at 500), with a star toggle to keep the good ones forever in a separate Favourites tab. Each row has four search links (Spotify / YouTube / Apple Music / Bandcamp) so you can find that track on whichever service you use. Filter by station, search by artist or title, clear history with a button. Per-user, nothing leaves your site.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.4.0</span> &mdash; 29 May 2026<br>
<?php esc_html_e( 'Now-playing visual indicator. Four tiny dancing bars next to "Now Playing" pulse while the audio is playing (pure CSS, always works). On top of that, a Web Audio frequency visualizer tries to draw live frequency bars on a small canvas — if the browser allows it and the stream is CORS-friendly it kicks in automatically; if not, the CSS bars stay and nothing breaks.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.3.2</span> &mdash; 29 May 2026<br>
<?php esc_html_e( 'Play-button glyph baseline fix. The dashicon used for play/pause was rendering below the button text. Swapped to a plain Unicode glyph (▶ / ‖) that sits on the text baseline like any other character.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.3.1</span> &mdash; 29 May 2026<br>
<?php esc_html_e( 'My Radio layout polish + dropped dark-auto. Player no longer stretches edge-to-edge (max-width 880px); volume slider fixed width so the % label sits next to it; station dropdown capped at 360px; play-button icon shrunk to match the button text baseline; intro paragraph now fits one line. Removed the prefers-color-scheme dark-auto behaviour that was unreadable against WordPress\'s still-white postbox; theme=dark is still available as an explicit choice and now gives the player its own dark surface so it actually reads.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.3.0</span> &mdash; 29 May 2026<br>
<?php esc_html_e( 'Dark theme wired through. Mute toggle on the speaker icon. OS media keys (F8 / headphone buttons / lock-screen) play and pause via MediaSession. Current track polled from SomaFM and shown under the station description while playing. Save errors surface as a brief notice instead of being swallowed. Genre badge restyled as an inline pill.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.2.0</span> &mdash; 26 May 2026<br>
<?php esc_html_e( 'UI rebuilt to WordPress admin standards. Postbox container, native Play/Pause button with text label, picks up your admin colour scheme via var(--wp-admin-theme-color), genre badge moved inline, dashboard widget no longer renders a card-inside-a-card. Functionality identical to v0.1.0 — purely visual polish.', 'radio' ); ?>
</li>
<li>
<span class="ver">v0.1.0</span> &mdash; 26 May 2026<br>
<?php esc_html_e( 'First release. 44 SomaFM stations grouped by 10 genres, dashboard widget + dedicated admin page, per-user state in user_meta, self-hosted update checker against Gitea. Direct HTML5 audio playback — no proxy, no build step, no tracking.', 'radio' ); ?>
</li>
</ul>
<a class="radio-about-changelog-link"
href="<?php echo esc_url( RADIO_GITEA_URL . '/src/branch/main/CHANGELOG.md' ); ?>"
target="_blank" rel="noopener">
<?php esc_html_e( 'View the full CHANGELOG.md →', 'radio' ); ?>
</a>
</div>
<div class="radio-about-card"> <div class="radio-about-card">
<h2><?php esc_html_e( 'Credits + thanks', 'radio' ); ?></h2> <h2><?php esc_html_e( 'Credits + thanks', 'radio' ); ?></h2>
<p> <p>
@@ -102,7 +67,39 @@ function radio_render_about_page() {
</p> </p>
</div> </div>
</div><!-- /.radio-about-grid -->
<!-- ── Version history: latest in full, earlier as one-liners ── -->
<div class="radio-about-versions">
<h2><?php esc_html_e( 'Version history', 'radio' ); ?></h2>
<div class="radio-about-versions__latest">
<span class="ver">v0.6.2</span> &mdash; 30 May 2026 <span class="latest"><?php esc_html_e( 'latest', 'radio' ); ?></span>
<p>
<?php esc_html_e( 'Current version badge on the Settings page. A small grey pill follows the "Radio — Settings" heading so the version you are running is visible at a glance — no need to hover the plugin in Plugins → Installed or open the About page to check.', 'radio' ); ?>
</p>
</div>
<h3><?php esc_html_e( 'Earlier releases', 'radio' ); ?></h3>
<ul class="radio-about-versions__earlier">
<li><span class="ver">v0.6.1</span> <span class="ver-date">30 May 2026</span> &mdash; <?php esc_html_e( 'About page restructure — balanced 3-card top row + compact version history', 'radio' ); ?></li>
<li><span class="ver">v0.6.0</span> <span class="ver-date">30 May 2026</span> &mdash; <?php esc_html_e( 'Pop-out mini-player — continuous background play across admin navigation', 'radio' ); ?></li>
<li><span class="ver">v0.5.0</span> <span class="ver-date">29 May 2026</span> &mdash; <?php esc_html_e( 'Track history + favourites (Spotify / YouTube / Apple Music / Bandcamp search links)', 'radio' ); ?></li>
<li><span class="ver">v0.4.0</span> <span class="ver-date">29 May 2026</span> &mdash; <?php esc_html_e( 'Now-playing indicator — dancing bars + Web Audio frequency visualizer', 'radio' ); ?></li>
<li><span class="ver">v0.3.2</span> <span class="ver-date">29 May 2026</span> &mdash; <?php esc_html_e( 'Play-button glyph baseline fix (dashicon → Unicode ▶ / ‖)', 'radio' ); ?></li>
<li><span class="ver">v0.3.1</span> <span class="ver-date">29 May 2026</span> &mdash; <?php esc_html_e( 'My Radio layout polish + dropped dark-auto', 'radio' ); ?></li>
<li><span class="ver">v0.3.0</span> <span class="ver-date">29 May 2026</span> &mdash; <?php esc_html_e( 'Dark theme + mute + OS media keys + SomaFM current-track display', 'radio' ); ?></li>
<li><span class="ver">v0.2.0</span> <span class="ver-date">26 May 2026</span> &mdash; <?php esc_html_e( 'UI rebuilt to WordPress admin standards', 'radio' ); ?></li>
<li><span class="ver">v0.1.0</span> <span class="ver-date">26 May 2026</span> &mdash; <?php esc_html_e( 'First release — 44 SomaFM stations, dashboard widget + dedicated admin page, per-user state, self-hosted Gitea updater', 'radio' ); ?></li>
</ul>
<a class="radio-about-changelog-link"
href="<?php echo esc_url( RADIO_GITEA_URL . '/src/branch/main/CHANGELOG.md' ); ?>"
target="_blank" rel="noopener">
<?php esc_html_e( 'View the full CHANGELOG.md →', 'radio' ); ?>
</a>
</div> </div>
</div> </div>
<?php <?php
} }
+4
View File
@@ -58,6 +58,10 @@ function radio_render_main_page() {
<span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span> <span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span>
</button> </button>
<button type="button" class="button radio-player__popout" data-radio-popout title="<?php esc_attr_e( 'Open in a pop-out window — keeps playing while you navigate the admin', 'radio' ); ?>">
<span aria-hidden="true">↗</span> <?php esc_html_e( 'Pop out', 'radio' ); ?>
</button>
<div class="radio-player__volume"> <div class="radio-player__volume">
<button type="button" class="radio-player__mute" data-radio-mute aria-label="<?php esc_attr_e( 'Mute', 'radio' ); ?>"> <button type="button" class="radio-player__mute" data-radio-mute aria-label="<?php esc_attr_e( 'Mute', 'radio' ); ?>">
<span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span> <span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span>
+4
View File
@@ -51,6 +51,10 @@ function radio_render_dashboard_widget() {
<span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span> <span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span>
</button> </button>
<button type="button" class="button radio-player__popout" data-radio-popout title="<?php esc_attr_e( 'Open in a pop-out window — keeps playing while you navigate', 'radio' ); ?>">
<span aria-hidden="true">↗</span> <?php esc_html_e( 'Pop out', 'radio' ); ?>
</button>
<div class="radio-player__volume"> <div class="radio-player__volume">
<button type="button" class="radio-player__mute" data-radio-mute aria-label="<?php esc_attr_e( 'Mute', 'radio' ); ?>"> <button type="button" class="radio-player__mute" data-radio-mute aria-label="<?php esc_attr_e( 'Mute', 'radio' ); ?>">
<span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span> <span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span>
+4 -1
View File
@@ -47,7 +47,10 @@ function radio_render_settings_page() {
$hide_widget = ! empty( $state['hide_dashboard_widget'] ); $hide_widget = ! empty( $state['hide_dashboard_widget'] );
?> ?>
<div class="wrap radio-settings-wrap"> <div class="wrap radio-settings-wrap">
<h1><?php esc_html_e( 'Radio — Settings', 'radio' ); ?></h1> <h1>
<?php esc_html_e( 'Radio — Settings', 'radio' ); ?>
<span class="radio-version-badge">v<?php echo esc_html( RADIO_VERSION ); ?></span>
</h1>
<form method="post" action=""> <form method="post" action="">
<?php wp_nonce_field( 'radio_save_settings', 'radio_settings_nonce' ); ?> <?php wp_nonce_field( 'radio_save_settings', 'radio_settings_nonce' ); ?>
+136 -2
View File
@@ -5,7 +5,7 @@
* Plugin Name: Radio * Plugin Name: Radio
* Plugin URI: https://icanhelp.ie/radio * Plugin URI: https://icanhelp.ie/radio
* Description: A small, focused, free radio player for your WordPress admin. 44 SomaFM stations grouped by 10 genres — ambient, electronic, lounge, rock, metal, jazz, world, reggae, holiday, specials. Plays via HTML5 audio; volume + station choice persist per-user. * Description: A small, focused, free radio player for your WordPress admin. 44 SomaFM stations grouped by 10 genres — ambient, electronic, lounge, rock, metal, jazz, world, reggae, holiday, specials. Plays via HTML5 audio; volume + station choice persist per-user.
* Version: 0.5.0 * Version: 0.6.2
* Requires at least: 5.0 * Requires at least: 5.0
* Requires PHP: 7.4 * Requires PHP: 7.4
* Author: David Keane * Author: David Keane
@@ -20,7 +20,7 @@
if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! defined( 'ABSPATH' ) ) { exit; }
// Plugin coordinates. // Plugin coordinates.
if ( ! defined( 'RADIO_VERSION' ) ) { define( 'RADIO_VERSION', '0.5.0' ); } if ( ! defined( 'RADIO_VERSION' ) ) { define( 'RADIO_VERSION', '0.6.2' ); }
if ( ! defined( 'RADIO_FILE' ) ) { define( 'RADIO_FILE', __FILE__ ); } if ( ! defined( 'RADIO_FILE' ) ) { define( 'RADIO_FILE', __FILE__ ); }
if ( ! defined( 'RADIO_PATH' ) ) { define( 'RADIO_PATH', plugin_dir_path( __FILE__ ) ); } if ( ! defined( 'RADIO_PATH' ) ) { define( 'RADIO_PATH', plugin_dir_path( __FILE__ ) ); }
if ( ! defined( 'RADIO_URL' ) ) { define( 'RADIO_URL', plugin_dir_url( __FILE__ ) ); } if ( ! defined( 'RADIO_URL' ) ) { define( 'RADIO_URL', plugin_dir_url( __FILE__ ) ); }
@@ -131,6 +131,7 @@ function radio_enqueue_admin_assets( $hook ) {
'stations' => radio_get_stations_flat(), 'stations' => radio_get_stations_flat(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'radio_save_state' ), 'nonce' => wp_create_nonce( 'radio_save_state' ),
'popoutUrl' => admin_url( 'admin-post.php?action=radio_popout' ),
'strings' => array( 'strings' => array(
'play' => __( 'Play', 'radio' ), 'play' => __( 'Play', 'radio' ),
'pause' => __( 'Pause', 'radio' ), 'pause' => __( 'Pause', 'radio' ),
@@ -221,6 +222,139 @@ function radio_ajax_clear_history() {
wp_send_json_success(); wp_send_json_success();
} }
/**
* v0.6.0 — Pop-out mini player. Renders a standalone HTML page (no WP
* admin chrome) at `admin-post.php?action=radio_popout`, opened by the
* JS via `window.open()`. The popup persists across main-tab navigation
* so background music keeps playing when the user moves around the
* admin. `&play=1` in the URL tells `radio.js` to auto-resume on load.
*/
add_action( 'admin_post_radio_popout', 'radio_render_popout_page' );
function radio_render_popout_page() {
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'radio' ) );
}
$state = radio_get_state();
$station = radio_find_station( $state['station_id'] );
$stations = radio_get_stations_grouped();
$theme = isset( $state['theme'] ) ? $state['theme'] : 'auto';
if ( ! in_array( $theme, array( 'auto', 'light', 'dark' ), true ) ) { $theme = 'auto'; }
$cfg = array(
'state' => $state,
'stations' => radio_get_stations_flat(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'radio_save_state' ),
'popoutUrl' => '', // already in popout — no further popouts
'autoPlay' => isset( $_GET['play'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- query flag only, no state change
'strings' => array(
'play' => __( 'Play', 'radio' ),
'pause' => __( 'Pause', 'radio' ),
'loading' => __( 'Loading…', 'radio' ),
'error' => __( 'Stream error — try another station.', 'radio' ),
'saveError' => __( 'Preferences not saved — check your connection.', 'radio' ),
'mute' => __( 'Mute', 'radio' ),
'unmute' => __( 'Unmute', 'radio' ),
'nowPlaying' => __( 'Now Playing', 'radio' ),
'volume' => __( 'Volume', 'radio' ),
'station' => __( 'Station', 'radio' ),
),
);
?>
<!DOCTYPE html>
<html lang="<?php echo esc_attr( str_replace( '_', '-', get_bloginfo( 'language' ) ) ); ?>">
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php printf( esc_html__( 'Radio — %s', 'radio' ), esc_html( $station['name'] ) ); ?></title>
<link rel="stylesheet" href="<?php echo esc_url( includes_url( 'css/dashicons.min.css' ) ); ?>?ver=<?php echo esc_attr( get_bloginfo( 'version' ) ); ?>">
<link rel="stylesheet" href="<?php echo esc_url( RADIO_URL . 'assets/css/radio.css' ); ?>?ver=<?php echo esc_attr( RADIO_VERSION ); ?>">
<style>
:root { --wp-admin-theme-color: #2271b1; }
html, body { margin: 0; padding: 0; }
body { padding: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f0f0f1; color: #1d2327; font-size: 13px; }
.radio-popout-header { display: flex; justify-content: space-between; align-items: center; margin: 0 0 12px; }
.radio-popout-header h1 { margin: 0; font-size: 14px; font-weight: 600; color: #1d2327; }
.radio-popout-header h1::before { content: '📻 '; }
.radio-popout-close { background: none; border: 0; font-size: 18px; cursor: pointer; color: #646970; padding: 4px 8px; line-height: 1; border-radius: 3px; }
.radio-popout-close:hover { color: #b32d2e; background: rgba(179,45,46,0.08); }
.radio-popout-wrap { background: #fff; border: 1px solid #c3c4c7; padding: 14px; border-radius: 3px; }
.radio-popout-wrap .radio-player__station-select select { max-width: 100%; }
.radio-popout-wrap .radio-player__volume { width: 100%; }
.radio-popout-wrap .radio-player__controls { flex-direction: column; align-items: stretch; gap: 10px; }
body.radio-theme-dark { background: #101213; color: #f0f0f1; }
body.radio-theme-dark .radio-popout-header h1 { color: #f0f0f1; }
body.radio-theme-dark .radio-popout-wrap { background: #1d2327; border-color: #3c434a; }
body.radio-theme-dark .radio-popout-close { color: #a7aaad; }
</style>
</head>
<body class="radio-popout radio-theme-<?php echo esc_attr( $theme ); ?>">
<div class="radio-popout-header">
<h1><?php esc_html_e( 'Radio', 'radio' ); ?></h1>
<button type="button" class="radio-popout-close" onclick="window.close()" title="<?php esc_attr_e( 'Close', 'radio' ); ?>">✕</button>
</div>
<div class="radio-popout-wrap">
<div class="radio-player" data-radio-surface="popout">
<div class="radio-player__now">
<span class="radio-player__indicator" aria-hidden="true">
<span class="radio-player__bars"><span></span><span></span><span></span><span></span></span>
<canvas class="radio-player__viz" data-radio-viz hidden></canvas>
</span>
<span class="radio-player__label"><?php esc_html_e( 'Now Playing', 'radio' ); ?></span>
<span class="radio-player__station-name" data-radio-name><?php echo esc_html( $station['name'] ); ?></span>
<span class="radio-player__station-genre" data-radio-genre><?php echo esc_html( $station['genre'] ); ?></span>
<p class="radio-player__station-desc" data-radio-desc><?php echo esc_html( $station['description'] ); ?></p>
<p class="radio-player__track" data-radio-track hidden></p>
</div>
<div class="radio-player__controls">
<button type="button" class="button button-primary radio-player__play" data-radio-play>
<span class="radio-player__play-glyph" data-radio-play-glyph aria-hidden="true">&#9654;</span>
<span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span>
</button>
<div class="radio-player__volume">
<button type="button" class="radio-player__mute" data-radio-mute aria-label="<?php esc_attr_e( 'Mute', 'radio' ); ?>">
<span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span>
</button>
<input type="range" min="0" max="100" value="<?php echo esc_attr( (int) round( $state['volume'] * 100 ) ); ?>" data-radio-volume aria-label="<?php esc_attr_e( 'Volume', 'radio' ); ?>">
<span class="radio-player__volume-pct" data-radio-volume-pct><?php echo esc_html( (int) round( $state['volume'] * 100 ) ); ?>%</span>
</div>
</div>
<div class="radio-player__station-select">
<label for="radio-station-popout"><?php esc_html_e( 'Station', 'radio' ); ?></label>
<select id="radio-station-popout" data-radio-station>
<?php foreach ( $stations as $genre => $entries ) :
if ( empty( $entries ) ) { continue; }
?>
<optgroup label="<?php echo esc_attr( $genre ); ?>">
<?php foreach ( $entries as $entry ) : ?>
<option value="<?php echo esc_attr( $entry['id'] ); ?>" data-url="<?php echo esc_attr( $entry['url'] ); ?>" data-desc="<?php echo esc_attr( $entry['description'] ); ?>" data-genre="<?php echo esc_attr( $entry['genre'] ); ?>" <?php selected( $entry['id'], $state['station_id'] ); ?>>
<?php echo esc_html( $entry['name'] ); ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</div>
<div class="radio-player__error" data-radio-error hidden></div>
<audio data-radio-audio preload="none" crossorigin="anonymous"></audio>
</div>
</div>
<script>window.RadioPlugin = <?php echo wp_json_encode( $cfg ); ?>;</script>
<script src="<?php echo esc_url( RADIO_URL . 'assets/js/radio.js' ); ?>?ver=<?php echo esc_attr( RADIO_VERSION ); ?>"></script>
</body>
</html>
<?php
exit;
}
/** /**
* Surface the user's theme choice (auto/light/dark) to CSS as a body * Surface the user's theme choice (auto/light/dark) to CSS as a body
* class. `radio-theme-dark` forces dark; `radio-theme-auto` lets the * class. `radio-theme-dark` forces dark; `radio-theme-auto` lets the