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 station sidebar --- */
|
||||||
.focus-preset-list {
|
/* --- Radio sidebar --- */
|
||||||
list-style: none;
|
.rsb-nowplaying { margin-bottom: 12px; }
|
||||||
display: flex;
|
.rsb-station-name { font-weight: 600; }
|
||||||
flex-direction: column;
|
.rsb-track { font-size: 0.85rem; margin-top: 2px; }
|
||||||
gap: 6px;
|
.rsb-controls { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
|
||||||
margin: 10px 0;
|
.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; }
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Table of contents sidebar --- */
|
/* --- Table of contents sidebar --- */
|
||||||
.toc-list {
|
.toc-list {
|
||||||
|
|
|
||||||
132
static/js/app.js
132
static/js/app.js
|
|
@ -2733,10 +2733,6 @@ let _isPinching = false;
|
||||||
if (typeof pdfjsLib !== 'undefined') {
|
if (typeof pdfjsLib !== 'undefined') {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js';
|
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() {
|
async function loadBookList() {
|
||||||
if (!IS_AUTHENTICATED) return;
|
if (!IS_AUTHENTICATED) return;
|
||||||
|
|
@ -3277,31 +3273,6 @@ async function openBook(bookId) {
|
||||||
_resizeObserver.observe(contentEl);
|
_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();
|
enterReaderImmersiveMode();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -3464,14 +3435,6 @@ function closeReader() {
|
||||||
// Remove PDF invert class
|
// Remove PDF invert class
|
||||||
if (overlay) overlay.classList.remove('pdf-inverted');
|
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() {
|
function openTocSidebar() {
|
||||||
|
|
@ -4570,86 +4533,37 @@ function dismissHighlightPopover() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Focus station sidebar
|
// Radio sidebar (compact player)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const FOCUS_STATION_PRESETS = [
|
function openRadioSidebar() {
|
||||||
{name: 'None (no station)', url: ''},
|
const stationName = currentStation ? escapeHtml(currentStation.name) : '— no station —';
|
||||||
{name: 'SomaFM Groove Salad', url: 'https://ice5.somafm.com/groovesalad-128-aac'},
|
const track = currentTrack ? escapeHtml(currentTrack) : '';
|
||||||
{name: 'SomaFM Deep Space One', url: 'https://ice5.somafm.com/deepspaceone-128-aac'},
|
const vol = document.getElementById('volume')?.value ?? 204;
|
||||||
{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 openFocusStationSidebar() {
|
const rows = [...document.querySelectorAll('#saved-tbody tr[data-id]')];
|
||||||
// null = never saved (default active); {url:''} = disabled; {url:'...'} = custom
|
const stationsHtml = rows.length
|
||||||
const effectiveUrl = USER_FOCUS_STATION === null ? DEFAULT_FOCUS_STATION.url : (USER_FOCUS_STATION.url || '');
|
? rows.map(r => {
|
||||||
const currentName = USER_FOCUS_STATION === null ? DEFAULT_FOCUS_STATION.name
|
const url = JSON.stringify(r.dataset.url || '');
|
||||||
: (USER_FOCUS_STATION.name || 'None (no station)');
|
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>`;
|
||||||
let presetsHtml = FOCUS_STATION_PRESETS.map((p, i) => {
|
}).join('')
|
||||||
const active = p.url === effectiveUrl ? ' class="focus-preset-active"' : '';
|
: `<li class="muted">No saved stations.</li>`;
|
||||||
return `<li${active}><button class="btn btn-sm" data-focus-preset="${i}">${escapeHtml(p.name)}</button></li>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<p class="muted">Station played when opening a book.</p>
|
<div class="rsb-nowplaying">
|
||||||
<p><strong>Current:</strong> ${escapeHtml(currentName)}</p>
|
<div class="rsb-station-name">${stationName}</div>
|
||||||
<ul class="focus-preset-list">${presetsHtml}</ul>
|
${track ? `<div class="rsb-track muted">${track}</div>` : ''}
|
||||||
<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>
|
</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);
|
openSidebar('Radio', 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) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-save" id="save-station-btn" style="display:none;" onclick="saveCurrentStation()">★ Save</button>
|
<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="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>
|
||||||
<div class="podcast-seek-bar" id="podcast-seek-bar" style="display:none;">
|
<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>
|
<button class="btn-icon skip-btn" onclick="skipBack()" title="Back 15s">⏪ 15</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue