fix(v0.5.3-prep): Quick Stations robust load + add history export
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.
This commit is contained in:
+66
-2
@@ -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 || '';
|
||||
|
||||
Reference in New Issue
Block a user