diff --git a/CHANGELOG.md b/CHANGELOG.md index 1427a83..90c2c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,41 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versi --- +## [0.5.0] — 2026-05-29 — Track history + favourites + +SomaFM plays deep cuts you'll never hear again. v0.5.0 quietly logs every track that scrolls past so you can find it again later — and a star button keeps the ones worth keeping forever. + +### Added — `Radio → History` admin page (per-user) +- **History tab** — capped FIFO list of the last 500 played tracks. Each row shows when (relative time, full timestamp on hover), station, *artist — title*, four search links, and a favourite-star toggle. +- **Favourites tab** — uncapped list of starred tracks. Same row layout. Survives even when the history rolls over. +- **Filter** by artist/title (live, client-side) and by station (dropdown). **Clear history** button on the History tab — favourites preserved. +- **Four search providers** per row, brand-tinted on hover: **Spotify** (green), **YouTube** (red), **Apple Music** (pink), **Bandcamp** (teal). Deep-link search URLs only — no API keys, no third-party JS. +- Empty-state messages on both tabs. + +### Added — automatic logging during playback +- `fetchTrack` (the existing 30s SomaFM polling loop) now hands every new track to a new `logTrackIfNew` helper that POSTs it to `wp_ajax_radio_log_track`. +- **Dedup**: client-side via `lastLoggedSig` so the 30s polling doesn't re-log the same song; server-side against the last entry in user_meta as belt-and-braces. +- **Junk filtering**: `(unknown)` artists and entries missing artist *or* title are dropped server-side in `radio_sanitize_entry`. + +### Added — per-user storage +- Two new `user_meta` keys, separate from `radio_state` so frequent track inserts don't churn the player-state blob: + - `radio_history` — capped at **500 entries** (~50–80 KB max). + - `radio_favourites` — uncapped, expected to stay small (user-curated). + +### Added — three new AJAX endpoints +- `wp_ajax_radio_log_track` — append a track (nonce: `radio_save_state`; player-page only). +- `wp_ajax_radio_toggle_favourite` — toggle a track in favourites (nonce: `radio_history`; History-page only). +- `wp_ajax_radio_clear_history` — clear the history list (nonce: `radio_history`; History-page only). Favourites untouched. + +### Notes +- Per-user — nothing is shared, nothing leaves the site. Just your own listening history on your own WP account. +- **No PII concern** — entries are public station/artist/title strings from SomaFM's own JSON. +- Search-link UI tints toward each provider's brand colour on hover only — keeps the row visually calm in the default state. + +**Files changed:** `radio.php` (version, require, submenu, asset enqueue hook, three AJAX endpoints, three new localized strings), **new** `inc/history.php` (storage helpers + page renderer), `assets/css/radio.css` (history-page table, toolbar, search-link pills, favourite star, dark-theme overrides), `assets/js/radio.js` (`logTrackIfNew` wired into `fetchTrack`; `bindHistoryPage` for filter/favourite/clear), `inc/about.php` (history entry). + +--- + ## [0.4.0] — 2026-05-29 — Now-playing indicator: dancing bars + Web Audio visualizer A small visual that instantly says *"this is playing right now."* Two layers — a reliable CSS-only indicator that always works, and a progressive Web Audio upgrade that draws actual frequency data when the browser allows. diff --git a/assets/css/radio.css b/assets/css/radio.css index 328adac..9f54646 100644 --- a/assets/css/radio.css +++ b/assets/css/radio.css @@ -453,3 +453,105 @@ border-radius: 3px; border: 1px solid #3c434a; } + +/* ────────────────────────────────────────────────────────────────── + * History + Favourites page (v0.5.0) + * ─────────────────────────────────────────────────────────────── */ + +.radio-history-wrap { max-width: 1100px; } + +.radio-tab-count { + color: #646970; + font-weight: 400; + margin-left: 2px; +} + +.radio-history-toolbar { + display: flex; + gap: 10px; + align-items: center; + margin: 12px 0 16px; + flex-wrap: wrap; +} + +.radio-history-toolbar input[type="search"] { + flex: 1 1 220px; + max-width: 360px; +} +.radio-history-toolbar select { max-width: 220px; } +.radio-history-clear { margin-left: auto !important; } + +.radio-history-empty { + margin-top: 24px; + padding: 24px; + background: #fff; + border: 1px solid #c3c4c7; + color: #50575e; + font-style: italic; + text-align: center; +} + +.radio-history-table { background: #fff; } +.radio-history-table th, +.radio-history-table td { padding: 10px 12px; vertical-align: middle; } +.radio-history-table th.when, +.radio-history-table th.station, +.radio-history-table th.fav { white-space: nowrap; } +.radio-history-table .when, +.radio-history-table .station { + color: #646970; font-size: 12px; white-space: nowrap; +} +.radio-history-table .track { font-size: 14px; } +.radio-history-table .track strong { color: #1d2327; } +.radio-history-table .search { white-space: nowrap; } +.radio-history-table .fav { text-align: center; width: 42px; } + +/* Search-link pills, brand-tinted on hover */ +.radio-search-link { + display: inline-block; + padding: 2px 8px; + margin-right: 4px; + font-size: 11px; + font-weight: 600; + border-radius: 10px; + text-decoration: none; + background: #e2e4e7; + color: #1d2327; + transition: background 0.15s ease, color 0.15s ease; +} +.radio-search-link:hover { background: #d0d3d6; } +.radio-search-link--spotify:hover { background: #1ed760; color: #000; } +.radio-search-link--youtube:hover { background: #ff0000; color: #fff; } +.radio-search-link--apple:hover { background: #fa57c1; color: #fff; } +.radio-search-link--bandcamp:hover { background: #629aa9; color: #fff; } + +/* Favourite star — grey by default, golden when starred */ +.radio-fav-btn { + background: none; + border: 0; + padding: 4px 6px; + cursor: pointer; + font-size: 18px; + line-height: 1; + color: #c3c4c7; + transition: transform 0.12s ease, color 0.12s ease; +} +.radio-fav-btn:hover { transform: scale(1.2); color: #f0b849; } +.radio-fav-btn:focus { outline: 2px solid var(--wp-admin-theme-color, #2271b1); outline-offset: 1px; } +.radio-fav-btn.is-fav { color: #f0b849; } +.radio-fav-btn[disabled] { opacity: 0.5; cursor: progress; } + +/* Filtered-out rows hidden via JS */ +.radio-history-row.is-filtered { display: none; } + +/* Dark theme — history surfaces */ +.radio-theme-dark .radio-history-table { background: #1d2327; color: #c3c4c7; border-color: #3c434a; } +.radio-theme-dark .radio-history-table th { background: #2c3338; color: #f0f0f1; border-bottom-color: #3c434a; } +.radio-theme-dark .radio-history-table .track strong { color: #f0f0f1; } +.radio-theme-dark .radio-history-table .when, +.radio-theme-dark .radio-history-table .station { color: #a7aaad; } +.radio-theme-dark .radio-search-link { background: #2c3338; color: #c3c4c7; } +.radio-theme-dark .radio-history-empty { background: #1d2327; border-color: #3c434a; color: #c3c4c7; } +.radio-theme-dark .radio-fav-btn { color: #50575e; } +.radio-theme-dark .radio-fav-btn.is-fav, +.radio-theme-dark .radio-fav-btn:hover { color: #f0b849; } diff --git a/assets/js/radio.js b/assets/js/radio.js index d9f4f1b..e3d9de1 100644 --- a/assets/js/radio.js +++ b/assets/js/radio.js @@ -109,7 +109,8 @@ } /** SomaFM current-track polling. Best-effort: stays hidden if the API - * is unreachable / CORS-blocked (the plugin keeps working regardless). */ + * is unreachable / CORS-blocked (the plugin keeps working regardless). + * v0.5.0: hands new tracks to logTrackIfNew so they land in user history. */ function startTrackPolling(player, station) { stopTrackPolling(player); var trackEl = player.querySelector('[data-radio-track]'); @@ -129,6 +130,7 @@ if (!title && !artist) { trackEl.hidden = true; return; } trackEl.textContent = artist ? (title + ' — ' + artist) : title; trackEl.hidden = false; + logTrackIfNew(artist, title, station); }) .catch(function () { trackEl.hidden = true; }); } @@ -136,6 +138,29 @@ player._trackTimer = setInterval(fetchTrack, 30000); } + /** v0.5.0: POST a newly-played track to the server history. + * Deduped client-side via lastLoggedSig so consecutive polls of the + * same song don't spam the endpoint (server dedups too as belt+braces). */ + var lastLoggedSig = ''; + function logTrackIfNew(artist, title, station) { + if (!cfg.ajaxUrl || !cfg.nonce) { return; } + if (!artist || !title) { return; } + if (artist.toLowerCase() === '(unknown)') { return; } + var sig = (artist + '|' + title + '|' + ((station && station.id) || '')).toLowerCase(); + if (sig === lastLoggedSig) { return; } + lastLoggedSig = sig; + + var fd = new FormData(); + fd.append('action', 'radio_log_track'); + fd.append('nonce', cfg.nonce); + fd.append('artist', artist); + fd.append('title', title); + fd.append('station', (station && station.name) || ''); + fd.append('station_id', (station && station.id) || ''); + fetch(cfg.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: fd }) + .catch(function () { /* track log is best-effort */ }); + } + function stopTrackPolling(player) { if (player._trackTimer) { clearInterval(player._trackTimer); @@ -435,10 +460,79 @@ }); } + /** v0.5.0: wire the History admin page — filter, favourite-toggle, + * clear-history. No-op on pages without these elements. */ + function bindHistoryPage() { + var searchIn = document.getElementById('radio-history-search'); + var stationSel = document.getElementById('radio-history-station'); + var rows = document.querySelectorAll('.radio-history-row'); + var clearBtn = document.getElementById('radio-history-clear'); + + function applyFilter() { + if (!rows.length) { return; } + var q = (searchIn ? searchIn.value : '').toLowerCase().trim(); + var s = stationSel ? stationSel.value : ''; + rows.forEach(function (row) { + var matchQ = !q || (row.dataset.search || '').indexOf(q) !== -1; + var matchS = !s || row.dataset.stationId === s; + row.classList.toggle('is-filtered', !(matchQ && matchS)); + }); + } + if (searchIn) { searchIn.addEventListener('input', applyFilter); } + if (stationSel) { stationSel.addEventListener('change', applyFilter); } + + // Favourite-toggle buttons (per row) + document.querySelectorAll('.radio-fav-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var fd = new FormData(); + fd.append('action', 'radio_toggle_favourite'); + fd.append('nonce', btn.dataset.nonce || ''); + fd.append('artist', btn.dataset.artist || ''); + fd.append('title', btn.dataset.title || ''); + fd.append('station', btn.dataset.station || ''); + fd.append('station_id', btn.dataset.stationId || ''); + btn.disabled = true; + fetch(cfg.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: fd }) + .then(function (r) { return r.json(); }) + .then(function (res) { + btn.disabled = false; + if (!res || !res.success) { return; } + var isFav = !!(res.data && res.data.favourite); + btn.classList.toggle('is-fav', isFav); + btn.textContent = isFav ? '★' : '☆'; + btn.setAttribute('aria-label', isFav + ? (cfg.strings.removeFav || 'Remove from favourites') + : (cfg.strings.addFav || 'Add to favourites')); + }) + .catch(function () { btn.disabled = false; }); + }); + }); + + // Clear-history button (history tab only) + if (clearBtn) { + clearBtn.addEventListener('click', function () { + var msg = cfg.strings.clearConfirm || 'Clear all track history? (Favourites are kept.)'; + if (!window.confirm(msg)) { return; } + var fd = new FormData(); + fd.append('action', 'radio_clear_history'); + fd.append('nonce', clearBtn.dataset.nonce || ''); + clearBtn.disabled = true; + fetch(cfg.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: fd }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res && res.success) { window.location.reload(); } + else { clearBtn.disabled = false; } + }) + .catch(function () { clearBtn.disabled = false; }); + }); + } + } + function init() { var players = document.querySelectorAll('.radio-player'); for (var i = 0; i < players.length; i++) { bindPlayer(players[i]); } bindSettingsSlider(); + bindHistoryPage(); } if (document.readyState === 'loading') { diff --git a/inc/about.php b/inc/about.php index d8830a9..a3ca4b4 100644 --- a/inc/about.php +++ b/inc/about.php @@ -48,7 +48,11 @@ function radio_render_about_page() {