Replace focus station sidebar with compact radio player sidebar
All checks were successful
Build and push Docker image / build (push) Successful in 13s
Test / test (push) Successful in 15s

The 📻 button now opens a sidebar with the currently playing station/track,
play/stop + volume control, and the saved stations list. The old focus
station configuration UI (presets, custom URL input, save-to-server) and
the auto-play-on-book-open behavior are removed.

Closes #6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-04-05 14:22:07 +02:00
parent 554ca93e30
commit e5dc58d84f
3 changed files with 31 additions and 127 deletions

View file

@ -1574,23 +1574,13 @@ body.dnd-mode .timer-display {
}
/* --- Focus station sidebar --- */
.focus-preset-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
margin: 10px 0;
}
.focus-preset-list li.focus-preset-active button {
border-color: var(--accent);
color: var(--accent);
}
.focus-custom-input {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
}
/* --- Radio sidebar --- */
.rsb-nowplaying { margin-bottom: 12px; }
.rsb-station-name { font-weight: 600; }
.rsb-track { font-size: 0.85rem; margin-top: 2px; }
.rsb-controls { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.rsb-vol { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; color: var(--muted, #888); }
.rsb-station-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
/* --- Table of contents sidebar --- */
.toc-list {

View file

@ -2733,10 +2733,6 @@ let _isPinching = false;
if (typeof pdfjsLib !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js';
}
const DEFAULT_FOCUS_STATION = {
url: 'https://ice5.somafm.com/groovesalad-128-aac',
name: 'SomaFM Groove Salad',
};
async function loadBookList() {
if (!IS_AUTHENTICATED) return;
@ -3277,31 +3273,6 @@ async function openBook(bookId) {
_resizeObserver.observe(contentEl);
}
// Determine which station to play (null = use default, {url:''} = disabled)
const focusStation = USER_FOCUS_STATION === null ? DEFAULT_FOCUS_STATION
: (USER_FOCUS_STATION.url ? USER_FOCUS_STATION : null);
if (focusStation) {
if (isPlaying) {
// Don't interrupt — highlight button, play on click instead
const btn = $('focus-station-btn');
if (btn) {
btn.classList.add('focus-pending');
btn.title = `Click to play focus station: ${focusStation.name}`;
btn._pendingFocusStation = focusStation;
btn.onclick = function () {
playStation(focusStation.url, focusStation.name, null);
btn.classList.remove('focus-pending');
btn.title = 'Focus station';
btn._pendingFocusStation = null;
btn.onclick = openFocusStationSidebar;
};
}
} else {
playStation(focusStation.url, focusStation.name, null);
}
}
enterReaderImmersiveMode();
} catch (e) {
@ -3464,14 +3435,6 @@ function closeReader() {
// Remove PDF invert class
if (overlay) overlay.classList.remove('pdf-inverted');
// Clear any pending focus station highlight
const btn = $('focus-station-btn');
if (btn && btn._pendingFocusStation) {
btn.classList.remove('focus-pending');
btn.title = 'Focus station';
btn._pendingFocusStation = null;
btn.onclick = openFocusStationSidebar;
}
}
function openTocSidebar() {
@ -4570,86 +4533,37 @@ function dismissHighlightPopover() {
}
// ---------------------------------------------------------------------------
// Focus station sidebar
// Radio sidebar (compact player)
// ---------------------------------------------------------------------------
const FOCUS_STATION_PRESETS = [
{name: 'None (no station)', url: ''},
{name: 'SomaFM Groove Salad', url: 'https://ice5.somafm.com/groovesalad-128-aac'},
{name: 'SomaFM Deep Space One', url: 'https://ice5.somafm.com/deepspaceone-128-aac'},
{name: 'SomaFM Drone Zone', url: 'https://ice5.somafm.com/dronezone-128-aac'},
{name: 'SomaFM Space Station', url: 'https://ice5.somafm.com/spacestation-128-aac'},
{name: 'Linn Jazz', url: 'http://radio.linnrecords.com/linnjazz.pls'},
];
function openRadioSidebar() {
const stationName = currentStation ? escapeHtml(currentStation.name) : '— no station —';
const track = currentTrack ? escapeHtml(currentTrack) : '';
const vol = document.getElementById('volume')?.value ?? 204;
function openFocusStationSidebar() {
// null = never saved (default active); {url:''} = disabled; {url:'...'} = custom
const effectiveUrl = USER_FOCUS_STATION === null ? DEFAULT_FOCUS_STATION.url : (USER_FOCUS_STATION.url || '');
const currentName = USER_FOCUS_STATION === null ? DEFAULT_FOCUS_STATION.name
: (USER_FOCUS_STATION.name || 'None (no station)');
let presetsHtml = FOCUS_STATION_PRESETS.map((p, i) => {
const active = p.url === effectiveUrl ? ' class="focus-preset-active"' : '';
return `<li${active}><button class="btn btn-sm" data-focus-preset="${i}">${escapeHtml(p.name)}</button></li>`;
}).join('');
const rows = [...document.querySelectorAll('#saved-tbody tr[data-id]')];
const stationsHtml = rows.length
? rows.map(r => {
const url = JSON.stringify(r.dataset.url || '');
const name = JSON.stringify(r.dataset.name || '');
return `<li><button class="btn btn-sm" onclick="playStation(${url},${name},null)">${escapeHtml(r.dataset.name || '')}</button></li>`;
}).join('')
: `<li class="muted">No saved stations.</li>`;
const html = `
<p class="muted">Station played when opening a book.</p>
<p><strong>Current:</strong> ${escapeHtml(currentName)}</p>
<ul class="focus-preset-list">${presetsHtml}</ul>
<div class="focus-custom-input">
<input type="text" id="focus-custom-name" class="search-input" placeholder="Station name" value="${escapeHtml(effectiveUrl ? currentName : '')}">
<input type="text" id="focus-custom-url" class="search-input" placeholder="Stream URL" value="${escapeHtml(effectiveUrl)}">
<button class="btn" data-focus-save="1">Save</button>
<button class="btn btn-sm" data-focus-play="1">Play Now</button>
<div class="rsb-nowplaying">
<div class="rsb-station-name">${stationName}</div>
${track ? `<div class="rsb-track muted">${track}</div>` : ''}
</div>
<div class="rsb-controls">
<button class="btn ${isPlaying ? 'playing' : ''}" onclick="togglePlayStop()">${isPlaying ? '⏹ Stop' : '▶ Play'}</button>
<label class="rsb-vol">vol <input type="range" id="rsb-volume" min="0" max="255" value="${vol}"
oninput="const v=this.value;document.getElementById('volume').value=v;document.getElementById('volume-num').value=v;audio.volume=v/255"></label>
</div>
<ul class="rsb-station-list">${stationsHtml}</ul>
`;
openSidebar('Focus Station', html);
const body = $('sidebar-body');
body.addEventListener('click', function _focusClick(e) {
const presetBtn = e.target.closest('[data-focus-preset]');
const saveBtn = e.target.closest('[data-focus-save]');
const playBtn = e.target.closest('[data-focus-play]');
if (presetBtn) {
const preset = FOCUS_STATION_PRESETS[parseInt(presetBtn.dataset.focusPreset, 10)];
if (preset) saveFocusStation(preset.url, preset.name);
body.removeEventListener('click', _focusClick);
} else if (saveBtn) {
const url = (body.querySelector('#focus-custom-url')?.value || '').trim();
const name = (body.querySelector('#focus-custom-name')?.value || '').trim();
saveFocusStation(url, name);
body.removeEventListener('click', _focusClick);
} else if (playBtn) {
const url = (body.querySelector('#focus-custom-url')?.value || '').trim();
const name = (body.querySelector('#focus-custom-name')?.value || '').trim();
if (url) playStation(url, name, null);
}
});
}
async function saveFocusStation(url, name) {
url = (url || '').trim();
name = (name || '').trim();
if (!IS_AUTHENTICATED) {
USER_FOCUS_STATION = {url, name};
closeSidebar();
return;
}
try {
const res = await fetch('/accounts/focus-station/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
body: JSON.stringify({url, name}),
});
const data = await res.json();
if (data.ok) {
USER_FOCUS_STATION = {url, name};
closeSidebar();
}
} catch (e) {}
openSidebar('Radio', html);
}
// ---------------------------------------------------------------------------

View file

@ -18,7 +18,7 @@
</label>
<button class="btn btn-save" id="save-station-btn" style="display:none;" onclick="saveCurrentStation()">&#9733; Save</button>
<button class="btn-icon" id="dnd-btn" onclick="toggleDND()" title="Focus mode (hides UI, press Esc to exit)"></button>
<button class="btn-icon" id="focus-station-btn" onclick="openFocusStationSidebar()" title="Focus station">📻</button>
<button class="btn-icon" id="focus-station-btn" onclick="openRadioSidebar()" title="Radio">📻</button>
</div>
<div class="podcast-seek-bar" id="podcast-seek-bar" style="display:none;">
<button class="btn-icon skip-btn" onclick="skipBack()" title="Back 15s">&thinsp;15</button>