From 60331b8350d849e6b005dff1753e2cb7e602975b Mon Sep 17 00:00:00 2001 From: David Keane Date: Wed, 10 Jun 2026 01:07:53 +0100 Subject: [PATCH] fix(v0.5.3-prep): Quick Stations robust load + add history export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/options/options.html | 5 +++ src/options/options.js | 68 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/options/options.html b/src/options/options.html index c34d1cc..3e40901 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -53,6 +53,11 @@ +
+ + +
+
diff --git a/src/options/options.js b/src/options/options.js index a28d5cf..2354b78 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -9,6 +9,7 @@ import { getHistoryCap, setHistoryCap, clearHistory, clearFavourites, clearAll, } from '../lib/history.js'; +import { listAllStations } from '../sources/index.js'; import { THEME_KEY, THEME_DEFAULT, VALID_THEMES, getTheme, setTheme, applyTheme, @@ -42,6 +43,9 @@ const els = { qsCount: document.getElementById('opt-qs-count'), qsList: document.getElementById('opt-qs-list'), qsReset: document.getElementById('opt-qs-reset'), + // Export history (v0.5.3) + exportJson: document.getElementById('opt-export-json'), + exportCsv: document.getElementById('opt-export-csv'), }; init().catch(err => { @@ -76,6 +80,9 @@ async function init() { await refreshStats(); }); + els.exportJson.addEventListener('click', () => exportHistoryAs('json')); + els.exportCsv.addEventListener('click', () => exportHistoryAs('csv')); + els.clearHistory.addEventListener('click', async () => { if (!confirm('Clear all track history? Favourites will be preserved.')) return; await clearHistory(); @@ -151,7 +158,24 @@ async function initThemeUI() { * tuner.quickStations. NewTab's storage.onChanged listener re-renders * the chip row instantly without a reload. */ async function initQuickStationsUI() { - const cache = (await chrome.storage.local.get('tuner.stationsCache'))['tuner.stationsCache'] || []; + // Try the NewTab cache first (fast path). + let cache = (await chrome.storage.local.get('tuner.stationsCache'))['tuner.stationsCache'] || []; + + // Fresh install fallback: if Options is opened BEFORE NewTab has ever + // populated the cache, fetch SomaFM channels directly + cache them. + if (!cache.length) { + try { + cache = await listAllStations(); + // Cache it back so future loads are instant. + await chrome.storage.local.set({ + 'tuner.stationsCache': cache, + 'tuner.stationsCachedAt': Date.now(), + }); + } catch (err) { + console.warn('Quick Stations: failed to fetch channel list', err); + } + } + // Stations come in as { id: "somafm:groovesalad", name, description, genre, ... } // Sort alphabetically by name for predictable picker order. const channels = [...cache].sort((a, b) => (a.name || '').localeCompare(b.name || '')); @@ -161,7 +185,7 @@ async function initQuickStationsUI() { if (!channels.length) { const li = document.createElement('li'); li.className = 'opt-qs-loading'; - li.textContent = 'Channel list not cached yet — open the New Tab Page once to populate it.'; + li.textContent = 'Could not load SomaFM channel list — check your network connection and reopen Settings.'; els.qsList.appendChild(li); return; } @@ -288,6 +312,46 @@ function formatBytes(n) { return `${(n / (1024 * 1024)).toFixed(2)} MB`; } +/** Download tuner.history as a JSON or CSV file. (v0.5.3) */ +async function exportHistoryAs(format) { + const history = await getHistory(); + if (!history.length) { + toast('No history to export yet — play some music first', 'error'); + return; + } + + let blob, ext, mime; + if (format === 'json') { + blob = new Blob([JSON.stringify(history, null, 2)], { type: 'application/json' }); + ext = 'json'; + } else { + // CSV — RFC 4180 quoting (doubled quotes, every cell quoted for safety). + const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`; + const header = 'artist,title,station,station_id,when_iso\n'; + const rows = history.map(e => [ + q(e.artist), + q(e.title), + q(e.station), + q(e.stationId), + q(e.at ? new Date(e.at).toISOString() : ''), + ].join(',')).join('\n'); + blob = new Blob([header + rows + '\n'], { type: 'text/csv;charset=utf-8' }); + ext = 'csv'; + } + + const ts = new Date().toISOString().slice(0, 10); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `rangerhq-tuner-history-${ts}.${ext}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + + toast(`Exported ${history.length} track${history.length === 1 ? '' : 's'} as ${ext.toUpperCase()}`); +} + function toast(message, tone) { els.toast.textContent = message; els.toast.dataset.tone = tone || '';