Replace focus station sidebar with compact radio player sidebar
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:
parent
554ca93e30
commit
e5dc58d84f
3 changed files with 31 additions and 127 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
132
static/js/app.js
132
static/js/app.js
|
|
@ -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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</label>
|
||||
<button class="btn btn-save" id="save-station-btn" style="display:none;" onclick="saveCurrentStation()">★ 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">⏪ 15</button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue