feat(v0.5.3-prep): user-customizable Quick Stations + descriptions
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.
This commit is contained in:
@@ -13,6 +13,10 @@ import {
|
||||
THEME_KEY, THEME_DEFAULT, VALID_THEMES,
|
||||
getTheme, setTheme, applyTheme,
|
||||
} from '../lib/theme.js';
|
||||
import {
|
||||
QUICK_STATIONS_KEY, DEFAULT_QUICK_IDS,
|
||||
getQuickStations, setQuickStations, resetQuickStations,
|
||||
} from '../lib/quick-stations.js';
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
stations: 'tuner.stationsCache',
|
||||
@@ -34,6 +38,10 @@ const els = {
|
||||
volumeDisplay: document.getElementById('opt-volume-display'),
|
||||
lastStation: document.getElementById('opt-last-station'),
|
||||
toast: document.getElementById('opt-toast'),
|
||||
// Quick Stations picker (v0.5.3)
|
||||
qsCount: document.getElementById('opt-qs-count'),
|
||||
qsList: document.getElementById('opt-qs-list'),
|
||||
qsReset: document.getElementById('opt-qs-reset'),
|
||||
};
|
||||
|
||||
init().catch(err => {
|
||||
@@ -48,6 +56,9 @@ async function init() {
|
||||
// cross-surface changes (popup/newtab pick a theme → Options reflects).
|
||||
await initThemeUI();
|
||||
|
||||
// Quick Stations — render checkbox list of all SomaFM channels.
|
||||
await initQuickStationsUI();
|
||||
|
||||
await refreshStats();
|
||||
await refreshPlayback();
|
||||
|
||||
@@ -101,6 +112,15 @@ async function init() {
|
||||
const radio = document.querySelector(`input[name="opt-theme"][value="${v}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
}
|
||||
if (changes[QUICK_STATIONS_KEY]) {
|
||||
// Re-check the boxes to match the new picks (in case another surface
|
||||
// wiped storage or reset to defaults).
|
||||
const newPicks = new Set(await getQuickStations());
|
||||
for (const cb of document.querySelectorAll('#opt-qs-list input[type=checkbox]')) {
|
||||
cb.checked = newPicks.has(cb.value);
|
||||
}
|
||||
updateQuickStationsCount(newPicks.size);
|
||||
}
|
||||
if (Object.keys(changes).some(k => k.startsWith('tuner.'))) {
|
||||
refreshStats();
|
||||
refreshPlayback();
|
||||
@@ -125,6 +145,99 @@ async function initThemeUI() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Render the Quick Stations picker — checkbox per SomaFM channel from
|
||||
* the cached catalogue, with description below each name. Toggling a
|
||||
* checkbox writes the current picks to chrome.storage.local under
|
||||
* 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'] || [];
|
||||
// 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 || ''));
|
||||
const picks = new Set(await getQuickStations());
|
||||
|
||||
els.qsList.innerHTML = '';
|
||||
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.';
|
||||
els.qsList.appendChild(li);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ch of channels) {
|
||||
const shortId = (ch.id || '').split(':')[1];
|
||||
if (!shortId) continue;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'opt-qs-row';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'opt-qs-label';
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = shortId;
|
||||
cb.checked = picks.has(shortId);
|
||||
cb.addEventListener('change', onQuickStationToggle);
|
||||
|
||||
const main = document.createElement('div');
|
||||
main.className = 'opt-qs-main';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'opt-qs-name';
|
||||
name.textContent = ch.name || shortId;
|
||||
|
||||
if (ch.genre) {
|
||||
const g = document.createElement('div');
|
||||
g.className = 'opt-qs-genre';
|
||||
g.textContent = ch.genre;
|
||||
main.appendChild(name);
|
||||
main.appendChild(g);
|
||||
} else {
|
||||
main.appendChild(name);
|
||||
}
|
||||
|
||||
if (ch.description) {
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'opt-qs-desc';
|
||||
desc.textContent = ch.description;
|
||||
main.appendChild(desc);
|
||||
}
|
||||
|
||||
label.appendChild(cb);
|
||||
label.appendChild(main);
|
||||
li.appendChild(label);
|
||||
els.qsList.appendChild(li);
|
||||
}
|
||||
|
||||
updateQuickStationsCount(picks.size);
|
||||
|
||||
els.qsReset.addEventListener('click', async () => {
|
||||
if (!confirm('Reset Quick Stations to the default 8?')) return;
|
||||
await resetQuickStations();
|
||||
// Re-check the boxes to match defaults
|
||||
const defaultSet = new Set(DEFAULT_QUICK_IDS);
|
||||
for (const cb of document.querySelectorAll('#opt-qs-list input[type=checkbox]')) {
|
||||
cb.checked = defaultSet.has(cb.value);
|
||||
}
|
||||
updateQuickStationsCount(defaultSet.size);
|
||||
toast('Quick Stations reset to defaults');
|
||||
});
|
||||
}
|
||||
|
||||
async function onQuickStationToggle() {
|
||||
const all = [...document.querySelectorAll('#opt-qs-list input[type=checkbox]')];
|
||||
const picked = all.filter(c => c.checked).map(c => c.value);
|
||||
await setQuickStations(picked);
|
||||
updateQuickStationsCount(picked.length);
|
||||
}
|
||||
|
||||
function updateQuickStationsCount(n) {
|
||||
els.qsCount.textContent = `${n} selected`;
|
||||
}
|
||||
|
||||
async function refreshStats() {
|
||||
const [history, favs, all] = await Promise.all([
|
||||
getHistory(),
|
||||
|
||||
Reference in New Issue
Block a user