Two fixes in one commit, both responding to David's testing of
v0.5.3-prep:
1. Quick Stations 'Loading...' stuck
The picker showed 'Loading channel list…' indefinitely when the
user opened Options BEFORE ever opening the New Tab Page (so the
tuner.stationsCache hadn't been populated yet). Fix:
initQuickStationsUI now falls back to calling listAllStations()
directly if the cache is empty, then writes the result back to
cache so future opens are instant. New error message if the network
fetch fails: 'Could not load SomaFM channel list — check your
network connection and reopen Settings.'
2. History export (JSON + CSV)
Added two buttons in the Local data card 'Export history (JSON)'
and 'Export history (CSV)'. JSON dumps the full tuner.history array
pretty-printed; CSV uses RFC 4180 quoting (every cell quoted, doubled
quotes for embedded quotes) with header artist,title,station,
station_id,when_iso. Filename: rangerhq-tuner-history-YYYY-MM-DD.{ext}.
Empty-history case shows an error toast. Toast confirms count + format
on success.
Per David: 'this app is kinda done' after this, then upload to Web
Store once v0.5.0 clears.
David flagged 2026-06-10: '14 stations wraps 3 lines, original idea
was 2 lines tidy, but now thought of user selecting themselves' +
'maybe have a description of each station' + 'be great to keep the
code clean.'
Architecture: pure data, not code. Adds tuner.quickStations as a
new chrome.storage.local key. Default 8 (tidy 2-row layout per the
v0.4.0 picks). User can pick any subset of the 46 SomaFM channels
via a new Options page card.
NEW FILE
src/lib/quick-stations.js — DEFAULT_QUICK_IDS + storage helpers
(getQuickStations, setQuickStations, resetQuickStations) with the
same defensive 'fall back if chrome.storage missing' pattern as
history.js + theme.js.
CHANGED
src/newtab/newtab.js — removed hardcoded QUICK_IDS array, replaced
with module-level 'let quickIds = []' populated during init from
getQuickStations() + re-loaded when chrome.storage.onChanged fires
on tuner.quickStations. renderQuick() now uses module-level
quickIds and shows 'No Quick Stations picked — set some in
Settings' empty state if the array is empty.
src/options/options.html — new 'Quick Stations' card between
Appearance and Playback. Contains: helper text + selected-count
badge + scrollable <ul> + Reset-to-defaults button.
src/options/options.css — ~90 lines for the new picker: scrollable
list (max-height 360px), checkbox-name-description row layout,
accent-coloured count badge, subtle hover background, brand-styled
scrollbar.
src/options/options.js — initQuickStationsUI() reads cached
channel list from tuner.stationsCache, sorts alphabetically, builds
one row per channel with checkbox + name + genre pill + description.
Toggle handler writes the current pick set to chrome.storage.local.
Reset button confirms then calls resetQuickStations() + re-checks
boxes to match defaults. Cross-surface sync via storage.onChanged
re-syncs check state if changes happen elsewhere.
5 files (1 new), ~250 lines added. No new permissions, no new
dependencies.
Adds an Auto / Dark / Light radio group to the Options page that
overrides the OS `prefers-color-scheme` setting. Stored in
chrome.storage.local under tuner.theme. Defaults to Auto.
Architecture:
- src/lib/theme.js (new, ~55 lines) — getTheme/setTheme/applyTheme/initTheme
helpers. applyTheme sets/removes `data-theme` attribute on <html>;
initTheme reads storage + applies.
- popup.js + newtab.js + options.js: call initTheme() FIRST in their
init() so the theme paints before anything else. All three also
listen for chrome.storage.onChanged on the THEME_KEY and live-apply
changes — pick Light in Options, popup + newtab flip instantly.
- options.html: new 'Appearance' card with 3 radio buttons (Auto/Dark/
Light) above the existing Playback card.
- options.css: styled radio group (pill-shaped, accent border on
:checked, hover state). Plus the Auto/Dark/Light CSS overrides
themselves.
- popup.css, newtab.css, options.css: each gets html[data-theme=light]
and html[data-theme=dark] blocks that override :root vars. Attribute
selector specificity beats both :root and the @media :root, so the
manual override wins when set.
UX:
- Default = Auto (follows OS via existing @media block)
- Pick Dark → overrides OS, forces dark palette
- Pick Light → overrides OS, forces light palette
- Selection persists across reloads, syncs across all three surfaces
7 files, ~150 lines added. No new permissions, no new dependencies.
Bundled with v0.4.1-prep back-link + Phase 1 OS-follow light mode for
the v0.5.0 release.
Web Store submission target. Mirrors rangerhq-radio's track-history pattern
(inc/history.php) so the family stays coherent across surfaces.
Highlights
- New Tab Page override (Tier 2.5) — Chrome's default new tab replaced with
a RangerHQ-branded landing showing the player, current track, quick chips,
searchable browse list, and now history + favourites tabs.
- Track history + favourites — capped FIFO 500, dedup against last entry,
skip "(unknown)" artist (SomaFM dead-air). Stored in chrome.storage.local
under tuner.history + tuner.favourites.
- 4-button search per entry — Spotify / YouTube / Apple Music / Bandcamp.
Pure public-search-URL link-outs in a new tab, NO auth, NO API keys, NO
quota, NO third-party SDK embedded.
- Options page (chrome://extensions → details → options) — live stats,
history cap slider (50-500), Clear history / Clear favourites / Clear
EVERYTHING buttons, About panel with Gitea + davidtkeane.com links.
- Popup nav row — Open in tab / History (#hash deep link) / Settings,
using chrome.tabs.create + chrome.runtime.openOptionsPage. No new perms.
- Cross-surface sync — popup ↔ newtab listen on chrome.storage.onChanged
for tuner.currentStationId / tuner.isPlaying / history / favourites.
- Storage gateway — offscreen doc can't reliably reach chrome.storage in
some Chrome versions, so it sends LOG_TRACK_REQUEST to the SW which
does the write. history.js also defensively guards every storage call.
- Metadata latency fix — polling now starts immediately on PLAY, in
parallel with audio buffer fill. First track display drops from
~10-15s to ~1-2s.
Permissions unchanged
- Still ["offscreen", "storage"] + somafm.com host only.
- chrome.tabs.create works on our own extension URLs without "tabs" perm.
- No webRequest, no <all_urls>, no third-party SDK.
Bumped from 0.1.0 (last tag on Gitea) directly to 0.3.0.
v0.2.0 (newtab + clock) was a working local build but never tagged;
its features ship together with v0.3.0 in this single commit.