feat: v0.2.0 — UI rebuilt to WordPress admin standards
v0.1.0 worked but felt like a third-party React widget bolted on
top of WordPress. v0.2.0 makes the player visually native to the
WP admin: postbox container, standard Play/Pause button with text
label, admin-colour-scheme aware accents, dashboard widget no
longer renders a card inside a card.
CHANGES
- inc/admin-page.php: main page now wraps the player in a
.postbox > .postbox-header (with h2.hndle) > .inside structure.
Custom rounded card / shadow stripped.
- inc/dashboard-widget.php: bare .radio-player content; WP already
wraps dashboard widgets in a postbox, was double-card before.
- inc/about.php: version-history card promotes v0.2.0 to latest,
demotes v0.1.0.
- assets/css/radio.css: rewrite. Strip custom shadows + oversized
typography. Adopt WP body-text defaults. Use
var(--wp-admin-theme-color, #2271b1) for volume-slider accent +
link colours so the plugin picks up whichever admin colour
scheme the user has chosen. About-page cards now use the
postbox-style gray header + 1px border pattern.
- assets/js/radio.js: setPlayIcon() also flips the visible text
label ("Play" ↔ "Pause"), not just the icon class. mirrorSelection()
also updates the [data-radio-genre] element so the genre label
stays in sync across surfaces.
- radio.php: Version: 0.1.0 -> 0.2.0; BUDDY_VERSION constant
bumped likewise.
- CHANGELOG.md: new [0.2.0] entry explaining the visual overhaul.
NET EFFECT
- Same 44 stations, same audio path, same persistence, same
updater, same AJAX endpoint. Pure visual change.
- The plugin now looks like part of WordPress admin instead of a
guest widget.
- Closer to WP.org submission criteria — plugin reviewers look
for native-styled plugins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,31 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [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
|
## [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.
|
**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.
|
||||||
|
|||||||
+136
-88
@@ -1,118 +1,109 @@
|
|||||||
/* Radio — admin styles */
|
/* Radio — admin styles, WordPress-native */
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────
|
||||||
|
* Player layout (works inside .postbox .inside on main page,
|
||||||
|
* and bare on the dashboard widget where .inside is the parent)
|
||||||
|
* ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.radio-player {
|
.radio-player {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Now Playing — left-aligned, body text size, no custom typography */
|
||||||
.radio-player__now {
|
.radio-player__now {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding-bottom: 6px;
|
align-items: baseline;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
flex-wrap: wrap;
|
||||||
}
|
gap: 8px;
|
||||||
|
padding-bottom: 10px;
|
||||||
.radio-player__now--large {
|
border-bottom: 1px solid #dcdcde;
|
||||||
padding: 18px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__label {
|
.radio-player__label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.06em;
|
||||||
color: #646970;
|
color: #646970;
|
||||||
margin-bottom: 4px;
|
font-weight: 600;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__station-name {
|
.radio-player__station-name {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1d2327;
|
color: #1d2327;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__now--large .radio-player__station-name {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-player__station-desc {
|
.radio-player__station-desc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #50575e;
|
color: #50575e;
|
||||||
margin-top: 2px;
|
flex: 1 1 100%;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__station-genre {
|
.radio-player__station-genre {
|
||||||
display: inline-block;
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 2px 10px;
|
|
||||||
background: #f0f0f1;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #2c3338;
|
color: #646970;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Controls — single row, native button + slider */
|
||||||
.radio-player__controls {
|
.radio-player__controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.radio-player__controls--large {
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__play {
|
.radio-player__play {
|
||||||
display: inline-flex;
|
/* native .button .button-primary styling; just ensure icon aligns */
|
||||||
|
display: inline-flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 6px;
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
line-height: 1 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__play .dashicons {
|
.radio-player__play .dashicons {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__controls--large .radio-player__play {
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-player__controls--large .radio-player__play .dashicons {
|
|
||||||
font-size: 28px;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-player__volume {
|
.radio-player__volume {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-player__volume .dashicons {
|
||||||
|
color: #646970;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__volume input[type="range"] {
|
.radio-player__volume input[type="range"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
accent-color: #2271b1;
|
margin: 0;
|
||||||
|
/* Use WP's admin theme colour for the slider thumb/track */
|
||||||
|
accent-color: var(--wp-admin-theme-color, #2271b1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__volume-pct {
|
.radio-player__volume-pct {
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #50575e;
|
color: #646970;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Station selector — label-on-top, full-width select */
|
||||||
.radio-player__station-select {
|
.radio-player__station-select {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -124,35 +115,38 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
color: #646970;
|
color: #646970;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__station-select select {
|
.radio-player__station-select select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 480px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Error notice — uses WP notice styling */
|
||||||
.radio-player__error {
|
.radio-player__error {
|
||||||
color: #b32d2e;
|
color: #b32d2e;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
background: #fcf0f1;
|
background: #fcf0f1;
|
||||||
border-left: 3px solid #b32d2e;
|
border-left: 4px solid #b32d2e;
|
||||||
border-radius: 2px;
|
border-radius: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.radio-player__credit {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #646970;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Credit footer */
|
||||||
|
.radio-player__credit {
|
||||||
|
margin: 0;
|
||||||
|
color: #646970;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.radio-player__credit--main {
|
.radio-player__credit--main {
|
||||||
margin-top: 18px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-player__credit a {
|
.radio-player__credit a {
|
||||||
color: #2271b1;
|
color: var(--wp-admin-theme-color, #2271b1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,54 +154,101 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main page wraps the player at a comfortable max width. */
|
/* ──────────────────────────────────────────────────────────────────
|
||||||
.radio-wrap .radio-player--main {
|
* Main admin page — wrap player in a postbox-like card
|
||||||
max-width: 640px;
|
* ─────────────────────────────────────────────────────────────── */
|
||||||
padding: 20px 24px;
|
|
||||||
background: #fff;
|
.radio-wrap .radio-player {
|
||||||
border: 1px solid #ccd0d4;
|
margin-top: 4px;
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-intro {
|
.radio-intro {
|
||||||
max-width: 640px;
|
margin: 0 0 16px;
|
||||||
color: #50575e;
|
color: #50575e;
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* About page layout */
|
/* ──────────────────────────────────────────────────────────────────
|
||||||
|
* Dashboard widget — no nested card; bare content inside .inside
|
||||||
|
* (WP renders the widget as a postbox already; don't double up)
|
||||||
|
* ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#radio_dashboard_widget .radio-player {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio_dashboard_widget .radio-player__now {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio_dashboard_widget .radio-player__station-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio_dashboard_widget .radio-player__credit {
|
||||||
|
border-top: 1px solid #dcdcde;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────
|
||||||
|
* About page — postbox-style cards in a metabox column layout
|
||||||
|
* ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.radio-about-grid {
|
.radio-about-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
gap: 18px;
|
gap: 16px;
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card {
|
.radio-about-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ccd0d4;
|
border: 1px solid #c3c4c7;
|
||||||
border-radius: 6px;
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
|
||||||
padding: 18px 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card h2 {
|
.radio-about-card h2 {
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-about-card > p,
|
||||||
|
.radio-about-card > ul,
|
||||||
|
.radio-about-card > a {
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-about-card > p + p {
|
||||||
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card--versions ul {
|
.radio-about-card--versions ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-left: 0;
|
padding: 12px;
|
||||||
padding-left: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card--versions li {
|
.radio-about-card--versions li {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-about-card--versions li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card--versions .ver {
|
.radio-about-card--versions .ver {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2271b1;
|
color: var(--wp-admin-theme-color, #2271b1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-card--versions .latest {
|
.radio-about-card--versions .latest {
|
||||||
@@ -219,10 +260,17 @@
|
|||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-about-changelog-link {
|
.radio-about-changelog-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 8px;
|
margin: 0 12px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: var(--wp-admin-theme-color, #2271b1);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-about-changelog-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -42,11 +42,16 @@
|
|||||||
}).catch(function () { /* swallow — local UI already updated */ });
|
}).catch(function () { /* swallow — local UI already updated */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update play/pause button icon to reflect current audio state. */
|
/** Update play/pause button icon + label to reflect current audio state. */
|
||||||
function setPlayIcon(btn, playing) {
|
function setPlayIcon(btn, playing) {
|
||||||
var icon = btn.querySelector('.dashicons');
|
var icon = btn.querySelector('.dashicons');
|
||||||
if (!icon) { return; }
|
var label = btn.querySelector('[data-radio-play-label]');
|
||||||
icon.className = 'dashicons ' + (playing ? 'dashicons-controls-pause' : 'dashicons-controls-play');
|
if (icon) {
|
||||||
|
icon.className = 'dashicons ' + (playing ? 'dashicons-controls-pause' : 'dashicons-controls-play');
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
label.textContent = playing ? (cfg.strings.pause || 'Pause') : (cfg.strings.play || 'Play');
|
||||||
|
}
|
||||||
btn.setAttribute('title', playing ? (cfg.strings.pause || 'Pause') : (cfg.strings.play || 'Play'));
|
btn.setAttribute('title', playing ? (cfg.strings.pause || 'Pause') : (cfg.strings.play || 'Play'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +127,8 @@
|
|||||||
audio.load();
|
audio.load();
|
||||||
if (nameEl) { nameEl.textContent = s.name; }
|
if (nameEl) { nameEl.textContent = s.name; }
|
||||||
if (descEl) { descEl.textContent = s.description || ''; }
|
if (descEl) { descEl.textContent = s.description || ''; }
|
||||||
|
var genreEl = player.querySelector('[data-radio-genre]');
|
||||||
|
if (genreEl) { genreEl.textContent = s.genre || ''; }
|
||||||
// Mirror selection to any OTHER .radio-player surfaces on the page.
|
// Mirror selection to any OTHER .radio-player surfaces on the page.
|
||||||
mirrorSelection(player, newId);
|
mirrorSelection(player, newId);
|
||||||
saveState({ station_id: newId });
|
saveState({ station_id: newId });
|
||||||
@@ -157,8 +164,10 @@
|
|||||||
if (s) {
|
if (s) {
|
||||||
var nm = other.querySelector('[data-radio-name]');
|
var nm = other.querySelector('[data-radio-name]');
|
||||||
var dc = other.querySelector('[data-radio-desc]');
|
var dc = other.querySelector('[data-radio-desc]');
|
||||||
|
var gn = other.querySelector('[data-radio-genre]');
|
||||||
if (nm) { nm.textContent = s.name; }
|
if (nm) { nm.textContent = s.name; }
|
||||||
if (dc) { dc.textContent = s.description || ''; }
|
if (dc) { dc.textContent = s.description || ''; }
|
||||||
|
if (gn) { gn.textContent = s.genre || ''; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-1
@@ -48,7 +48,11 @@ function radio_render_about_page() {
|
|||||||
<h2><?php esc_html_e( 'Version history', 'radio' ); ?></h2>
|
<h2><?php esc_html_e( 'Version history', 'radio' ); ?></h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span class="ver">v0.1.0</span> — 26 May 2026 <span class="latest">latest</span><br>
|
<span class="ver">v0.2.0</span> — 26 May 2026 <span class="latest">latest</span><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> — 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' ); ?>
|
<?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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+50
-40
@@ -2,9 +2,10 @@
|
|||||||
/**
|
/**
|
||||||
* Radio — main admin page (WP Admin → Radio → My Radio).
|
* Radio — main admin page (WP Admin → Radio → My Radio).
|
||||||
*
|
*
|
||||||
* Larger player with the same controls as the dashboard widget. Both
|
* Uses WP-native postbox structure so the player feels like part of
|
||||||
* surfaces share the same JS (assets/js/radio.js binds to every
|
* WordPress, not a third-party widget. The dashboard widget shares
|
||||||
* .radio-player on the page).
|
* the same internal markup via assets/js/radio.js binding to every
|
||||||
|
* .radio-player on the page.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||||
@@ -26,55 +27,64 @@ function radio_render_main_page() {
|
|||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: %d = number of stations */
|
/* translators: %d = number of stations */
|
||||||
esc_html__( 'A friendly tab of background music for your WordPress admin. %d hand-curated SomaFM stations across 10 genres, all free, no ads.', 'radio' ),
|
esc_html__( 'A tab of background music for your WordPress admin. %d hand-curated SomaFM stations across 10 genres — free, no ads, no tracking.', 'radio' ),
|
||||||
(int) $count
|
(int) $count
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="radio-player radio-player--main" data-radio-surface="main">
|
<div class="postbox">
|
||||||
<div class="radio-player__now radio-player__now--large">
|
<div class="postbox-header">
|
||||||
<div class="radio-player__label"><?php esc_html_e( 'Now Playing', 'radio' ); ?></div>
|
<h2 class="hndle"><?php esc_html_e( 'Player', 'radio' ); ?></h2>
|
||||||
<div class="radio-player__station-name" data-radio-name><?php echo esc_html( $station['name'] ); ?></div>
|
|
||||||
<div class="radio-player__station-desc" data-radio-desc><?php echo esc_html( $station['description'] ); ?></div>
|
|
||||||
<div class="radio-player__station-genre"><?php echo esc_html( $station['genre'] ); ?></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inside">
|
||||||
|
<div class="radio-player" data-radio-surface="main">
|
||||||
|
|
||||||
<div class="radio-player__controls radio-player__controls--large">
|
<div class="radio-player__now">
|
||||||
<button type="button" class="radio-player__play button button-primary button-hero" data-radio-play title="<?php esc_attr_e( 'Play', 'radio' ); ?>">
|
<span class="radio-player__label"><?php esc_html_e( 'Now Playing', 'radio' ); ?></span>
|
||||||
<span class="dashicons dashicons-controls-play"></span>
|
<span class="radio-player__station-name" data-radio-name><?php echo esc_html( $station['name'] ); ?></span>
|
||||||
</button>
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="radio-player__volume">
|
<div class="radio-player__controls">
|
||||||
<span class="dashicons dashicons-controls-volumeon"></span>
|
<button type="button" class="button button-primary radio-player__play" data-radio-play>
|
||||||
<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="dashicons dashicons-controls-play" aria-hidden="true"></span>
|
||||||
<span class="radio-player__volume-pct" data-radio-volume-pct><?php echo esc_html( (int) round( $state['volume'] * 100 ) ); ?>%</span>
|
<span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="radio-player__volume">
|
||||||
|
<span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span>
|
||||||
|
<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-main"><?php esc_html_e( 'Station', 'radio' ); ?></label>
|
||||||
|
<select id="radio-station-main" 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"></audio>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="radio-player__station-select">
|
|
||||||
<label for="radio-station-main"><?php esc_html_e( 'Station', 'radio' ); ?></label>
|
|
||||||
<select id="radio-station-main" 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"></audio>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="radio-player__credit radio-player__credit--main">
|
<p class="radio-player__credit radio-player__credit--main description">
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
wp_kses(
|
wp_kses(
|
||||||
|
|||||||
+15
-12
@@ -2,9 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* Radio — dashboard widget.
|
* Radio — dashboard widget.
|
||||||
*
|
*
|
||||||
* The compact mini-player on WP Admin → Dashboard. Same HTML structure
|
* The compact mini-player on WP Admin → Dashboard. WordPress wraps
|
||||||
* as the main admin page but smaller, so the assets/js/radio.js can
|
* dashboard widgets in a .postbox automatically, so we render bare
|
||||||
* bind to whichever surface the user opens first.
|
* content here — no nested card styling.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||||
@@ -31,21 +31,25 @@ function radio_render_dashboard_widget() {
|
|||||||
$station = radio_find_station( $state['station_id'] );
|
$station = radio_find_station( $state['station_id'] );
|
||||||
$stations = radio_get_stations_grouped();
|
$stations = radio_get_stations_grouped();
|
||||||
?>
|
?>
|
||||||
<div class="radio-player radio-player--widget" data-radio-surface="widget">
|
<div class="radio-player" data-radio-surface="widget">
|
||||||
|
|
||||||
<div class="radio-player__now">
|
<div class="radio-player__now">
|
||||||
<div class="radio-player__label"><?php esc_html_e( 'Now Playing', 'radio' ); ?></div>
|
<span class="radio-player__label"><?php esc_html_e( 'Now Playing', 'radio' ); ?></span>
|
||||||
<div class="radio-player__station-name" data-radio-name><?php echo esc_html( $station['name'] ); ?></div>
|
<span class="radio-player__station-name" data-radio-name><?php echo esc_html( $station['name'] ); ?></span>
|
||||||
<div class="radio-player__station-desc" data-radio-desc><?php echo esc_html( $station['description'] ); ?></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="radio-player__controls">
|
<div class="radio-player__controls">
|
||||||
<button type="button" class="radio-player__play button button-primary" data-radio-play title="<?php esc_attr_e( 'Play', 'radio' ); ?>">
|
<button type="button" class="button button-primary radio-player__play" data-radio-play>
|
||||||
<span class="dashicons dashicons-controls-play"></span>
|
<span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
|
||||||
|
<span data-radio-play-label><?php esc_html_e( 'Play', 'radio' ); ?></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="radio-player__volume">
|
<div class="radio-player__volume">
|
||||||
<span class="dashicons dashicons-controls-volumeon"></span>
|
<span class="dashicons dashicons-controls-volumeon" aria-hidden="true"></span>
|
||||||
<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' ); ?>">
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ function radio_render_dashboard_widget() {
|
|||||||
?>
|
?>
|
||||||
<optgroup label="<?php echo esc_attr( $genre ); ?>">
|
<optgroup label="<?php echo esc_attr( $genre ); ?>">
|
||||||
<?php foreach ( $entries as $entry ) : ?>
|
<?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'] ); ?>" <?php selected( $entry['id'], $state['station_id'] ); ?>>
|
<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'] ); ?>
|
<?php echo esc_html( $entry['name'] ); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -72,7 +76,6 @@ function radio_render_dashboard_widget() {
|
|||||||
|
|
||||||
<p class="radio-player__credit">
|
<p class="radio-player__credit">
|
||||||
<?php
|
<?php
|
||||||
/* translators: %s = SomaFM link */
|
|
||||||
printf(
|
printf(
|
||||||
wp_kses(
|
wp_kses(
|
||||||
/* translators: %s = link to somafm.com */
|
/* translators: %s = link to somafm.com */
|
||||||
|
|||||||
@@ -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.1.0
|
* Version: 0.2.0
|
||||||
* 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.1.0' ); }
|
if ( ! defined( 'RADIO_VERSION' ) ) { define( 'RADIO_VERSION', '0.2.0' ); }
|
||||||
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__ ) ); }
|
||||||
|
|||||||
Reference in New Issue
Block a user