// RangerHQ Tuner — Options page controller. // Manages the user's local data: history cap, clear actions, stats display. // Broadcasts STORAGE_WIPED after destructive actions so the popup/newtab // rerender immediately. import { TARGETS, TYPES } from '../lib/messages.js'; import { getHistory, getFavourites, getHistoryCap, setHistoryCap, clearHistory, clearFavourites, clearAll, } from '../lib/history.js'; const STORAGE_KEYS = { stations: 'tuner.stationsCache', currentId: 'tuner.currentStationId', volume: 'tuner.volume', }; const els = { version: document.getElementById('opt-version'), statHistory: document.getElementById('opt-stat-history'), statFavs: document.getElementById('opt-stat-favs'), statCache: document.getElementById('opt-stat-cache'), statBytes: document.getElementById('opt-stat-bytes'), capSlider: document.getElementById('opt-cap'), capValue: document.getElementById('opt-cap-value'), clearHistory: document.getElementById('opt-clear-history'), clearFavs: document.getElementById('opt-clear-favs'), clearAll: document.getElementById('opt-clear-all'), volumeDisplay: document.getElementById('opt-volume-display'), lastStation: document.getElementById('opt-last-station'), toast: document.getElementById('opt-toast'), }; init().catch(err => { console.error('Options init failed:', err); toast(`Init failed: ${err.message}`, 'error'); }); async function init() { els.version.textContent = `v${chrome.runtime.getManifest().version}`; await refreshStats(); await refreshPlayback(); const cap = await getHistoryCap(); els.capSlider.value = String(cap); els.capValue.textContent = String(cap); els.capSlider.addEventListener('input', () => { els.capValue.textContent = els.capSlider.value; }); els.capSlider.addEventListener('change', async () => { const newCap = await setHistoryCap(Number(els.capSlider.value)); els.capValue.textContent = String(newCap); toast(`History cap set to ${newCap}`); await refreshStats(); }); els.clearHistory.addEventListener('click', async () => { if (!confirm('Clear all track history? Favourites will be preserved.')) return; await clearHistory(); await refreshStats(); notifyUis(); toast('History cleared'); }); els.clearFavs.addEventListener('click', async () => { if (!confirm('Clear all favourites?')) return; await clearFavourites(); await refreshStats(); notifyUis(); toast('Favourites cleared'); }); els.clearAll.addEventListener('click', async () => { if (!confirm('Wipe EVERYTHING — history, favourites, station cache, volume, last station? This cannot be undone.')) return; await clearAll(); await refreshStats(); await refreshPlayback(); els.capSlider.value = '500'; els.capValue.textContent = '500'; notifyUis(); toast('All local data wiped', 'danger'); }); // Refresh stats live if storage changes from another surface chrome.storage.onChanged.addListener((changes, area) => { if (area !== 'local') return; if (Object.keys(changes).some(k => k.startsWith('tuner.'))) { refreshStats(); refreshPlayback(); } }); } async function refreshStats() { const [history, favs, all] = await Promise.all([ getHistory(), getFavourites(), chrome.storage.local.get(null), ]); const cacheArr = Array.isArray(all[STORAGE_KEYS.stations]) ? all[STORAGE_KEYS.stations] : []; // Estimate "tuner.*" bytes — JSON stringification is close enough for // a user-facing display, no need for the exact storage-engine cost. let bytes = 0; for (const [k, v] of Object.entries(all)) { if (!k.startsWith('tuner.')) continue; try { bytes += k.length + JSON.stringify(v).length; } catch { /* nope */ } } els.statHistory.textContent = `${history.length} tracks`; els.statFavs.textContent = `${favs.length} tracks`; els.statCache.textContent = `${cacheArr.length} stations`; els.statBytes.textContent = formatBytes(bytes); } async function refreshPlayback() { const stored = await chrome.storage.local.get([ STORAGE_KEYS.volume, STORAGE_KEYS.currentId, STORAGE_KEYS.stations, ]); const vol = stored[STORAGE_KEYS.volume]; els.volumeDisplay.textContent = typeof vol === 'number' ? `${Math.round(vol * 100)}%` : '—'; const id = stored[STORAGE_KEYS.currentId]; const cache = Array.isArray(stored[STORAGE_KEYS.stations]) ? stored[STORAGE_KEYS.stations] : []; const cur = cache.find(s => s.id === id); els.lastStation.textContent = cur?.name || (id || '—'); } function notifyUis() { chrome.runtime.sendMessage({ target: TARGETS.POPUP, type: TYPES.STORAGE_WIPED }) .catch(() => {}); // no listeners is fine } function formatBytes(n) { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / (1024 * 1024)).toFixed(2)} MB`; } function toast(message, tone) { els.toast.textContent = message; els.toast.dataset.tone = tone || ''; els.toast.hidden = false; clearTimeout(toast._t); toast._t = setTimeout(() => { els.toast.hidden = true; }, 2400); }