Files
rangerhq-tuner/CHANGELOG.md
T
ranger 04dc3b70ec chore: v0.5.1 — bump version + CHANGELOG entry
manifest.json 0.5.0 → 0.5.1. CHANGELOG.md entry for the
Quick Stations 8 → 14 expansion (merged from v0.5.1-prep,
commit 609e0ed).

Tagged on Gitea but NOT uploaded to Chrome Web Store until
v0.5.0 clears re-review. Per David: 'we will ship to web store
after the last one gets accepted, that one is important so a
user knows what to do' — the v0.5.0 first-run hint + light
mode + theme toggle are higher-value features and should land
in users' browsers before this stations expansion piles on top.
2026-06-10 00:32:42 +01:00

367 lines
23 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 **RangerHQ Tuner** are documented here.
Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versioning: [SemVer](https://semver.org/).
---
## [Unreleased]
### Planned — Future polish
- `.m3u` parser alongside `.pls` to widen future-adapter compatibility
- Station artwork lazy-load + fallback to family helmet
- Better error UI for failed streams ("Stream unavailable, try another")
- `.crx` packaging instructions in README for sideload users
- Second source adapter stub to exercise the multi-source UI
---
## [0.5.1] — 2026-06-10 — Quick Stations expansion (8 → 14)
Third update of the day. **Tagged on Gitea but held from Chrome Web Store upload until v0.5.0 clears re-review** — we want the v0.5.0 features (light mode + theme toggle + back-link + first-run UX hint) landing in users' browsers before this stations expansion piles on top.
### Added — 6 more channels in the NewTab Quick Stations chip row
Was reading sparse with only 8 chips. The row had space for more. Added six channels for broader genre coverage + two on-brand picks:
| Slug | Channel | Why |
|---|---|---|
| `defcon` | DEF CON Radio | Electronic / hacker culture — David's personal favourite, on-brand for a cybersecurity-adjacent extension |
| `beatblender` | Beat Blender | Electronic / breakbeat — popular channel |
| `bootliquor` | Boot Liquor | Americana / outlaw country — adds country/twang coverage |
| `u80s` | Underground 80s | 80s indie / synthwave — adds decade coverage |
| `bagel` | BAGeL Radio | Indie / alt-rock — guitar-driven indie |
| `thistle` | ThistleRadio | Celtic / folk — 🇮🇪 Dublin-built nod |
Total chip count: 8 → 14. No new files, no new permissions, no new dependencies, no code change other than the `QUICK_IDS` array in `src/newtab/newtab.js`.
The render loop already drops any slug missing from SomaFM's `channels.json` catalogue, so if a channel is retired upstream the chip just disappears silently — no error, no broken state.
### Files touched
- `src/newtab/newtab.js``QUICK_IDS` array updated from 8 to 14 entries, with per-slug genre comments. +19 / -8 lines.
Total: 1 file, +19/-8 lines.
### Not changed
- No new permissions
- No new dependencies
- No CSS / HTML changes
- No data migration
### Shipping note
`v0.5.1` is tagged on Gitea on 2026-06-10 but **deliberately held from the Chrome Web Store** until `v0.5.0` clears re-review (currently pending). David's reasoning: *"we will ship to web store after the last one gets accepted, that one is important so a user knows what to do"* — the v0.5.0 first-run hint + light mode features are higher-value for new users; this stations expansion is a smaller polish that should land after.
---
## [0.5.0] — 2026-06-10 — Light mode + theme toggle + Options back-link
Second post-launch update, ~36 hours after v0.4.0 cleared the Chrome Web Store. Three bundled improvements, all UX polish:
### Added — Light mode support across all three surfaces
**Phase 1 — Auto-follow OS theme.** Each stylesheet (popup, NewTab, Options) gets a `@media (prefers-color-scheme: light)` block that flips the `:root` CSS variables to a light palette while preserving brand identity. Every existing rule using `var(--*)` tokens carries over unchanged.
Light palette:
- `--bg` `#f6f4ed` cream-leaning off-white
- `--bg-soft` `#ece5d2` slightly darker for header bars
- `--bg-row` `#ddd6c0` hover backgrounds
- `--bg-row-hi` `#c8c0a8` active rows
- `--fg` `#2a2f28` dark forest-green-leaning text
- `--accent` `#2a7d3e` darker brand green (preserves "RangerHQ green" identity)
- `--cream` `#b8861a` darker amber (readable on light)
- `--danger` `#b53a2b` darker red (readable on light)
NewTab helmet watermark opacity bumped 0.025 → 0.04 in light mode (helmet contrasts differently on light bg). Wired via a new `--watermark-opacity` CSS variable.
State pills, primary play button, toasts, and danger button hover all get text-colour overrides for both modes so they stay readable when their background becomes solid accent / cream / danger.
### Added — Manual Auto / Dark / Light theme toggle (Phase 2)
**Three-radio group in a new "Appearance" card on the Options page.** Stored in `chrome.storage.local` under `tuner.theme`. Defaults to "auto" (follow OS).
- **Auto** — follows OS via `@media (prefers-color-scheme: light)` (covers ~95% of users)
- **Dark** — forces dark palette regardless of OS
- **Light** — forces light palette regardless of OS
This exists because there are three independent theme layers (OS, browser-chrome, page CSS) that can disagree. `@media (prefers-color-scheme: ...)` reads the OS layer only, NOT the browser-chrome theme. So users with mismatched layers (e.g. macOS light + Chrome dark theme installed) can override the auto-follow.
Implementation: `data-theme` attribute on `<html>` ( "light" / "dark" / removed for auto). `html[data-theme="..."]` CSS selectors beat both the default `:root` and the `@media :root` on specificity.
New file: `src/lib/theme.js` — shared `getTheme/setTheme/applyTheme/initTheme` helpers used by all three entry-point scripts.
The choice syncs across surfaces via `chrome.storage.onChanged`: pick Light in Options → popup + NewTab flip instantly without reload. Theme applies BEFORE first paint (each init() calls `initTheme()` first) so there's no dark flash on light-mode-preferring browsers.
### Added — Options page → Tuner back-link
The Options page header (helmet + "RangerHQ Tuner — Options") is now a single clickable anchor pointing at `newtab.html`. Subtle `←` glyph to the left of the helmet; hover shifts the arrow left and tints it accent green. Same-tab navigation — user came IN via Options, goes OUT into the player UI in the same tab. No accumulating Tuner tabs.
Fixes UX gap David flagged on 2026-06-09 night: *"when i click the settings button i go to settings but we have no back link to radio."*
### Files touched
- `src/lib/theme.js` (NEW, ~55 lines)
- `src/popup/popup.css` (+50 lines — light + dark override blocks)
- `src/popup/popup.js` (+8 lines — initTheme call + storage.onChanged hook)
- `src/newtab/newtab.css` (+50 lines — light + dark override blocks)
- `src/newtab/newtab.js` (+8 lines — same wiring)
- `src/options/options.html` (+24 lines — Appearance card)
- `src/options/options.css` (+90 lines — light + dark blocks + radio styling)
- `src/options/options.js` (+32 lines — radio handlers + cross-surface sync)
Total: 8 files, ~340 lines added.
### Not changed
- No new permissions
- No new host_permissions
- No new external libraries
- No data migration required
- Existing user state (current station / volume / history / favourites) survives intact
### Reviewer expectations
Same-account update with no permission change. Same-day review expected (~hours to 24h), similar to v0.4.0's 24h-ish re-review window per [[reference_chrome_web_store_rules]].
---
## [0.4.0] — 2026-06-09 — First-run UX hint
First post-launch update, shipped the same day v0.3.0 went LIVE on the Chrome Web Store. Pure UX polish — zero new permissions, zero new code dependencies, zero behaviour change for existing users.
### Added — Discoverable "pick a station to begin" affordance
Triggered by David's own 30-second panic on first Web Store install (uninstalled the dev build, installed from the Web Store fresh, hit Play, got nothing, then realised "ah, I need to pick a station first"). The product worked correctly — the dev build had a seeded station from hours of testing, the fresh install does not — but the 30-second panic exposed a real first-run-UX gap.
**Two layered cues, both pure CSS driven by a `body.is-first-run` class:**
1. **Subtle accent-green glow pulses** around the station list (popup) and the Quick Stations chip row (NewTab). Uses a 2.4-second `box-shadow` keyframe at low alpha (0.18-0.25) — visible but not noisy.
2. **Bouncing ↓ arrow** appended to the "Pick a station to begin" text in both surfaces. Uses an `::after` pseudo-element with a 1.8-second `translateY` keyframe.
The `is-first-run` class is toggled by a tiny `reflectFirstRunHint()` function called from:
- `init()` once stations + `currentStation` are resolved
- `onPickStation()` the moment a user picks
- the `chrome.storage.onChanged` listener when another surface picks (so the hint disappears on both surfaces simultaneously via cross-surface sync)
Existing users with a stored `tuner.currentStationId` never see either cue — the class only attaches when `currentStation` is null.
### Files touched
- `src/popup/popup.css` (+35 lines — keyframes + `.is-first-run` rules)
- `src/popup/popup.js` (+11 lines — `reflectFirstRunHint()` + 3 call sites)
- `src/newtab/newtab.css` (+36 lines — same idea, NewTab-namespaced)
- `src/newtab/newtab.js` (+10 lines — same pattern)
Total: 4 files, +92 lines, 0 deletions.
### Not changed
- No new permissions
- No new host_permissions
- No new external libraries
- No change to `manifest.json` beyond the version bump and CHANGELOG-referenced URL
- No data migration required (no storage shape change)
### Same-day context
This update ships the same day:
- v0.3.0 went LIVE on the Chrome Web Store (~17:08 Dublin, ~15.5h after submission)
- RangerHQ Radio v1.0.0 stability milestone went LIVE on WordPress.org (~21:51 Dublin)
- David received a PhD-prep signal from his Research in Computing lecturer at NCI Dublin
A solid day.
---
## [0.3.0] — 2026-06-08
### Added — Track history + 4-button search + Options page (Web Store target)
This is the version that goes to the Chrome Web Store. Mirrors the rangerhq-radio WordPress plugin's history feature so the family stays coherent across surfaces.
#### Track history
- **`src/lib/history.js`** — helpers for history/favourites/search-URL/dedup/cap/clear, mirroring `inc/history.php` in the WP plugin.
- **Storage:** `tuner.history` (FIFO, capped at 500 by default) and `tuner.favourites` (uncapped) in `chrome.storage.local`. Cap configurable 50500 on the Options page.
- **Entry shape** (mirror WP plugin exactly): `{ artist, title, station, stationId, at }` where `at = Date.now()`.
- **Dedup against the LAST entry only** so consecutive identical now-playing reads don't pile up.
- **Skip `(unknown)` artist** — SomaFM's dead-air/promo placeholder doesn't pollute the log.
#### Metadata polling
- **`src/offscreen/offscreen.js`** — the offscreen document (where audio lives) now polls SomaFM's per-channel song endpoint every 25 s while playing. On `PLAY`, polling starts (immediate first fetch + interval). On `PAUSE` or `ERROR`, polling stops. Within-session dedup prevents redundant logs when the same song stays current across polls.
- **Why the offscreen doc, not the SW:** the SW gets killed when idle; the offscreen doc is alive while audio plays. This is the right home for the loop.
- New message types in `src/lib/messages.js`: `TRACK_LOGGED`, `FAVOURITE_TOGGLED`, `STORAGE_WIPED`.
#### 4-button search (the headline feature)
- Every track row in History and Favourites gets four search-link buttons that open the major services' public search pages in a new tab:
| Service | URL pattern |
|---|---|
| Spotify | `https://open.spotify.com/search/{enc}` |
| YouTube | `https://www.youtube.com/results?search_query={enc}` |
| Apple Music | `https://music.apple.com/search?term={enc}` |
| Bandcamp | `https://bandcamp.com/search?q={enc}` |
Where `enc = encodeURIComponent(artist + " " + title)`. **Zero auth, zero API keys, zero quota, zero ToS gray area.** RangerHQ Tuner doesn't play these services — it opens the search results page and the user picks. Same approach as the rangerhq-radio WP plugin.
- Each link is brand-coloured (muted on the dark palette) — Spotify green, YouTube red, Apple pink, Bandcamp cyan. `target="_blank"` + `rel="noopener noreferrer"`.
#### Tabbed New Tab Page
- The New Tab Page's bottom section is now a **3-tab control**: **Stations · History · Favourites**, plus a ⚙ icon that opens the Options page.
- History tab: newest first, with track title, artist (accent green), `station · 2m ago` meta line, star toggle, and the 4 search links.
- Favourites tab: same shape, all your starred tracks.
- Each tab has its own "Clear …" button in the toolbar (confirm dialog).
#### Options page (NEW)
- Registered via `"options_page": "src/options/options.html"` in the manifest. Opens via `chrome://extensions` → details → Extension options OR via the ⚙ on the New Tab Page.
- **Local data card:** live stats (History size, Favourites size, Stations cached, Total `tuner.*` bytes), history-cap slider (50500 step 50), and three buttons:
- `Clear history` — drops `tuner.history` only.
- `Clear favourites` — drops `tuner.favourites` only.
- `Clear EVERYTHING` (danger styling) — wipes every `tuner.*` key in `chrome.storage.local`.
- **Playback card:** current volume %, last-played station name (read-only).
- **About card:** version, Gitea repo link, davidtkeane.com link, SomaFM link, licence reminder.
- A small toast message confirms each destructive action.
- The page listens for `chrome.storage.onChanged` so stats stay live if you wipe from somewhere else.
#### Manifest
- Version: `0.2.0``0.3.0`.
- Description updated to mention the 4-button search affordance (helps Web Store reviewers see the single-purpose statement matches the listing).
- Added `"options_page": "src/options/options.html"`.
- **No new permissions, no new host permissions.** Still `["offscreen", "storage"]` + `somafm.com` only. Search-link clicks open in a new browser tab — that's the user's navigation, not an API call from the extension.
#### Cross-surface state sync
The popup and the New Tab Page now stay in lockstep without manual reloads. Both surfaces subscribe to `chrome.storage.onChanged` and re-render when:
- `tuner.currentStationId` changes (someone in the other surface picked a station)
- `tuner.isPlaying` changes (play/pause from anywhere)
- `tuner.history` or `tuner.favourites` mutates (re-render the relevant tab)
Single source of truth = `chrome.storage.local`. No more drift between popup and New Tab.
#### Storage gateway architecture (MV3 safety net)
Offscreen documents don't reliably have access to `chrome.storage` across Chrome versions, so all storage writes from the offscreen audio host are routed through the service worker via a new `LOG_TRACK_REQUEST` message type. The SW (which always has `chrome.storage`) does the actual `tuner.history` write and broadcasts `TRACK_LOGGED` back to the UI surfaces.
As belt-and-braces, every function in `src/lib/history.js` now checks for `chrome.storage` availability before using it and returns sensible defaults if absent — so the module is safe to import from any extension context.
#### Metadata latency fix (parallelised audio + polling)
When PLAY arrives at the offscreen doc, metadata polling now starts **immediately** in parallel with the audio HTTP buffer fill, rather than waiting for `audio.play()` to resolve. First-track display time drops from ~10-15 seconds (audio buffer + then poll) to ~1-2 seconds (poll racing in parallel with buffer fill).
#### Popup quick-link nav
The toolbar popup now has a three-button nav row above the footer:
- **⎘ Open in tab** — `chrome.tabs.create({url: chrome.runtime.getURL('src/newtab/newtab.html')})` opens the full Tuner UI as a regular pinnable tab.
- **♪ History** — same as Open in tab but with `#history` hash so the History pane is pre-selected. `newtab.js` reads `location.hash` on init and respects it.
- **⚙ Settings** — `chrome.runtime.openOptionsPage()` opens the Options page directly.
No new permissions: `chrome.tabs.create()` works on our own extension URLs without the `tabs` permission. The Web Store permission profile stays `["offscreen", "storage"]` + `somafm.com` host only.
#### Why this is the Web Store version
- Clear single-purpose statement now obvious: *"Plays SomaFM internet radio, logs heard tracks, and provides search-link shortcuts to Spotify/YouTube/Apple Music/Bandcamp."*
- Privacy story stays clean: "collects nothing, stores everything locally, no remote storage" — every Options-page checkbox stays "does not collect" on the Dashboard.
- The four service buttons are pure `<a href>` link-outs — Web Store reviewers can verify they don't embed third-party SDKs.
- All inline styles are gone (lessons from `rangerhq-buddy v0.1.5`).
- Accessibility: every button has an `aria-label`, every interactive element is reachable by keyboard, focus rings visible, contrast ≥ 4.5:1.
- Storage operations are SW-gated, so even Chrome versions with the offscreen-doc storage quirk run cleanly.
---
## [0.2.0] — 2026-06-08
### Added — New Tab Page override (Tier 2.5)
Replace Chrome's default New Tab Page with a RangerHQ-branded landing that surfaces the player, current track, a quick-station chip row, and a searchable browse list. Same `chrome.offscreen` audio pipeline as the popup — the New Tab Page is just a second view onto the same playback state. Live ticking clock (HH:MM in bold + small dim seconds) updates every second; date renders in the user's locale.
Note: v0.2.0 was a working local build but was never tagged on Gitea — its features ship to the world together with v0.3.0 in a single commit.
- **`manifest.json`**: added
```json
"chrome_url_overrides": { "newtab": "src/newtab/newtab.html" }
```
Description string updated to mention the optional New Tab Page replacement (Web Store reviewers prefer overrides to be disclosed in the description).
- **`src/newtab/newtab.html`** — full-viewport landing with header (helmet + clock), large now-playing block, generous Play/Volume controls, quick-station chips, searchable browse list, footer linking to `davidtkeane.com` + Gitea.
- **`src/newtab/newtab.css`** — same earthy palette as the popup but laid out for a full screen. Subtle helmet watermark (2.5% opacity) in the background. Responsive shrink-to-fit at `max-height: 700px`.
- **`src/newtab/newtab.js`** — reuses `src/lib/messages.js`, `src/sources/index.js`, and the same `chrome.storage.local` keys as the popup. State stays consistent: pick a station in the popup, refresh new tab → same station selected, same play state. Adds a clock that updates every 30 s.
**Quick-pick stations:** the chip row surfaces the 8 most-loved SomaFM channels (Groove Salad, Drone Zone, Indie Pop Rocks!, Secret Agent, Space Station Soma, Lush, Deep Space One, Fluid). One click → playing.
**Why this feature exists:** validates [[user_browser_resident_tools]] — David has the browser open all day, every new tab is a touchpoint. Every `Cmd+T` now opens a RangerHQ-branded landing that's one click from playing radio. The Web Store reviewer is also more likely to grant a clear single-purpose override than a vague one — having a useful New Tab Page is good signal alongside the toolbar player.
**Permissions unchanged:** no new permissions or hosts added. The override is a pure UI choice; no expanded API surface.
---
## [0.1.0] — 2026-06-08
### Added — Tier 1 MVP (Buddy is alive — er, Tuner is alive)
**Buddy's browser cousin lives.** First release of RangerHQ Tuner — a Chrome Manifest V3 extension that plays SomaFM internet radio from your toolbar. Sibling to [`rangerhq-radio`](https://git.davidtkeane.com/ranger/rangerhq-radio) (the WordPress version live on wp.org since 2026-06-04). Same brand idea, different surface: WP version lives in admin pages, Chrome version lives one toolbar-click away no matter what you're doing.
#### Architecture
- **Manifest V3** — uses the `chrome.offscreen` API to host the `<audio>` element in a hidden document. Service workers can't host audio in MV3 (they get killed when idle), so a one-off offscreen document is the only supported pattern. Working code in `src/offscreen/offscreen.js` + `src/background/service-worker.js`.
- **Source-adapter pattern** at `src/sources/` — every radio network is a single file conforming to the `RadioSource` interface in `base-source.js`. Adding a new network = drop a new file + register one line in `src/sources/index.js`. Popup, SW, and offscreen never know which network is which.
- **Vanilla JS, no build step** — pure ES modules loaded directly. No webpack, no Vite, no `npm install`. Same ethos as the RangerHQ WP family (hand-rolled PHP).
- **No telemetry, no third-party JS** — everything is local. Only outbound network calls are to SomaFM's public endpoints (catalogue + stream metadata).
#### Features
- **Toolbar popup** showing a searchable SomaFM station list (~150 channels) with artwork, name, and genre.
- **Play / Pause / Volume** controls. Volume persists across browser restarts.
- **Last station** picked is remembered — Tuner remembers where you left off.
- **Now-playing metadata** displayed from SomaFM's per-channel song history endpoint.
- **Catalogue cached** in `chrome.storage.local` for 6 hours to make popup re-opens instant.
- **Audio continues after popup closes** — the offscreen document owns the playback, popup is just a view.
- **RangerHQ helmet** icon in the Chrome toolbar (resized from `src/assets/img/ranger.png` to 16/32/48/128 PNGs with a dark `#1a221c` padded background that matches the popup palette).
#### Files (22 created)
```
rangerhq-tuner/
├── manifest.json
├── README.md
├── CHANGELOG.md
├── .gitignore
└── src/
├── assets/
│ ├── icons/icon-{16,32,48,128}.png (toolbar icons)
│ └── img/ranger.png (master helmet logo, 257×275)
├── background/service-worker.js (message router only)
├── offscreen/offscreen.{html,js} (audio host — the MV3 gotcha solved)
├── popup/popup.{html,css,js} (the toolbar UI)
├── sources/
│ ├── base-source.js (RadioSource interface contract)
│ ├── somafm.js (first concrete adapter)
│ └── index.js (registry)
└── lib/
├── messages.js (type + target constants)
└── playlist-parser.js (.pls parser)
```
#### Manifest permissions (narrow on purpose)
```json
"permissions": ["offscreen", "storage"],
"host_permissions": ["https://somafm.com/*", "https://*.somafm.com/*"]
```
No `tabs`, no `<all_urls>`, no `webRequest`. Smallest permission ask possible = easiest Web Store review.
#### Architecture / status
- **Tier 1 MVP** — shipped this commit.
- **Tier 2** (planned) — full UX polish, favourites via `chrome.storage.sync`, now-playing polling loop, `.m3u` parser, `.crx` packaging, second source adapter stub.
- **Tier 3** (planned) — Chrome Web Store submission. See `~/.ranger-memory/docs/` for the per-family submission checklist; Chrome Web Store specifics tracked in the user's memory under `reference_chrome_web_store_rules`.
#### Why this exists
David has Chrome open essentially all day. The WP version of RangerHQ Radio requires going to admin pages to use; the toolbar version is one click away regardless of context. Same code-level effort, dramatically higher daily-use payoff. The "best thing we done so far" verdict on first sound (2026-06-08 evening) confirmed the bet.
#### Rangers lead the way 🪖☕🎵