Simplify chapter page audio player UX
- Remove play button and gear icon from chapter nav bar - Show bottom mini-player bar always (not slide-up on play) - Remove duplicate collapsed-bar seek track; keep only expanded panel scrub bar - Move gear/settings button into the bottom mini-player bar - Move settings panel into mini-player aside, opening upward - Clean up JS: remove navBtn/navIcon refs, showPlayer/hidePlayer, playerSeekTrack refs
This commit is contained in:
@@ -1882,24 +1882,6 @@ const chapterTmpl = `
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<!-- TTS play/pause (icon-only) -->
|
||||
<button id="tts-btn"
|
||||
type="button"
|
||||
onclick="ttsToggle()"
|
||||
aria-label="Listen"
|
||||
class="flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-full bg-amber-500 hover:bg-amber-400 text-zinc-950 text-base border-none cursor-pointer transition-colors">
|
||||
<span id="tts-icon" aria-hidden="true">▶</span>
|
||||
</button>
|
||||
|
||||
<!-- Settings -->
|
||||
<button id="settings-btn"
|
||||
type="button"
|
||||
onclick="toggleSettings()"
|
||||
aria-label="Reader settings"
|
||||
aria-haspopup="dialog"
|
||||
class="flex-shrink-0 px-2 py-1.5 rounded-lg bg-transparent text-zinc-400 hover:text-amber-400 text-base border-none cursor-pointer transition-colors">
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TTS status strip -->
|
||||
@@ -1937,31 +1919,6 @@ const chapterTmpl = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings panel -->
|
||||
<div id="settings-panel"
|
||||
role="dialog"
|
||||
aria-label="Reader settings"
|
||||
hidden
|
||||
class="absolute right-[max(0.5rem,calc(50%-32rem+0.5rem))] top-[calc(100%+0.25rem)] min-w-[260px] bg-zinc-900 border border-zinc-800 rounded-xl p-4 shadow-2xl z-[100]">
|
||||
<label class="block mb-3.5">
|
||||
<span class="block text-xs text-zinc-500 mb-1.5">Voice</span>
|
||||
<select id="tts-voice"
|
||||
class="w-full rounded-lg bg-zinc-800 border border-zinc-700 px-2 py-1.5 text-sm text-zinc-200 outline-none">
|
||||
{{range .Voices}}
|
||||
<option value="{{.}}"{{if eq . $.DefaultVoice}} selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block mb-3.5">
|
||||
<span class="block text-xs text-zinc-500 mb-1.5">Speed — <span id="tts-speed-label">1.0×</span></span>
|
||||
<input id="tts-speed" type="range"
|
||||
min="0.5" max="2" step="0.1" value="1"
|
||||
class="w-full accent-amber-500 cursor-pointer" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input id="tts-autoplay" type="checkbox" class="accent-amber-500 cursor-pointer w-4 h-4" />
|
||||
<span class="text-sm text-zinc-300">Auto-play next chapter</span>
|
||||
</label>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -2001,10 +1958,37 @@ const chapterTmpl = `
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<!-- ─── Mini audio player (slides up from bottom when audio starts) ────────── -->
|
||||
<!-- ─── Mini audio player (always visible at bottom) ────────────────────── -->
|
||||
<aside id="mini-player"
|
||||
aria-label="Audio player"
|
||||
class="fixed bottom-0 left-0 right-0 z-60 bg-zinc-950 border-t border-zinc-800 text-zinc-100 translate-y-full transition-transform duration-300 ease-out">
|
||||
class="fixed bottom-0 left-0 right-0 z-60 bg-zinc-950 border-t border-zinc-800 text-zinc-100">
|
||||
|
||||
<!-- Settings panel (opens upward) -->
|
||||
<div id="settings-panel"
|
||||
role="dialog"
|
||||
aria-label="Reader settings"
|
||||
hidden
|
||||
class="absolute right-[max(0.5rem,calc(50%-32rem+0.5rem))] bottom-[calc(100%+0.25rem)] min-w-[260px] bg-zinc-900 border border-zinc-800 rounded-xl p-4 shadow-2xl z-[100]">
|
||||
<label class="block mb-3.5">
|
||||
<span class="block text-xs text-zinc-500 mb-1.5">Voice</span>
|
||||
<select id="tts-voice"
|
||||
class="w-full rounded-lg bg-zinc-800 border border-zinc-700 px-2 py-1.5 text-sm text-zinc-200 outline-none">
|
||||
{{range .Voices}}
|
||||
<option value="{{.}}"{{if eq . $.DefaultVoice}} selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block mb-3.5">
|
||||
<span class="block text-xs text-zinc-500 mb-1.5">Speed — <span id="tts-speed-label">1.0×</span></span>
|
||||
<input id="tts-speed" type="range"
|
||||
min="0.5" max="2" step="0.1" value="1"
|
||||
class="w-full accent-amber-500 cursor-pointer" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input id="tts-autoplay" type="checkbox" class="accent-amber-500 cursor-pointer w-4 h-4" />
|
||||
<span class="text-sm text-zinc-300">Auto-play next chapter</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed bar: play/pause · title · time · expand -->
|
||||
<div id="player-bar"
|
||||
@@ -2019,15 +2003,9 @@ const chapterTmpl = `
|
||||
<span id="player-play-icon" aria-hidden="true">▶</span>
|
||||
</button>
|
||||
|
||||
<!-- Chapter title + inline mini progress -->
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-center gap-0.5">
|
||||
<!-- Chapter title -->
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-center">
|
||||
<span id="player-title" class="text-[0.8rem] font-medium text-zinc-200 truncate">—</span>
|
||||
<!-- Tap-to-seek progress track -->
|
||||
<div id="player-seek-track"
|
||||
role="slider" aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
|
||||
class="relative w-full h-[4px] bg-zinc-700 rounded-full cursor-pointer">
|
||||
<div id="player-seek-fill" class="absolute left-0 top-0 h-full bg-amber-500 rounded-full pointer-events-none" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time display -->
|
||||
@@ -2036,6 +2014,16 @@ const chapterTmpl = `
|
||||
<span id="player-time-tot" class="text-zinc-600">0:00</span>
|
||||
</div>
|
||||
|
||||
<!-- Settings button -->
|
||||
<button id="settings-btn"
|
||||
type="button"
|
||||
onclick="toggleSettings()"
|
||||
aria-label="Reader settings"
|
||||
aria-haspopup="dialog"
|
||||
class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-lg bg-transparent border-none cursor-pointer text-zinc-500 hover:text-amber-400 text-base transition-colors">
|
||||
⚙
|
||||
</button>
|
||||
|
||||
<!-- Expand toggle -->
|
||||
<button id="player-expand-btn"
|
||||
type="button"
|
||||
@@ -2124,12 +2112,10 @@ const chapterTmpl = `
|
||||
.queue-badge-paused { background: #1c1917; color: #d6d3d1; }
|
||||
.queue-badge-error { background: #450a0a; color: #f87171; }
|
||||
|
||||
/* Mini-player slide-up */
|
||||
#mini-player { will-change: transform; }
|
||||
#mini-player.player-visible { transform: translateY(0); }
|
||||
/* Mini-player is always visible on chapter pages */
|
||||
|
||||
/* Seek track hit-area padding for easier touch */
|
||||
#player-seek-track, #player-full-track {
|
||||
#player-full-track {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
margin-top: -6px;
|
||||
@@ -2156,8 +2142,6 @@ const chapterTmpl = `
|
||||
|
||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||
var audio = document.getElementById('tts-audio');
|
||||
var navBtn = document.getElementById('tts-btn'); // nav play/pause circle
|
||||
var navIcon = document.getElementById('tts-icon');
|
||||
var statusEl = document.getElementById('tts-status');
|
||||
var statusBar = document.getElementById('tts-status-bar');
|
||||
var voiceSel = document.getElementById('tts-voice');
|
||||
@@ -2171,8 +2155,6 @@ const chapterTmpl = `
|
||||
var playerPlayBtn = document.getElementById('player-play-btn');
|
||||
var playerPlayIcon = document.getElementById('player-play-icon');
|
||||
var playerTitle = document.getElementById('player-title');
|
||||
var playerSeekTrack = document.getElementById('player-seek-track');
|
||||
var playerSeekFill = document.getElementById('player-seek-fill');
|
||||
var playerTimeCur = document.getElementById('player-time-cur');
|
||||
var playerTimeTot = document.getElementById('player-time-tot');
|
||||
var playerExpandBtn = document.getElementById('player-expand-btn');
|
||||
@@ -2206,8 +2188,8 @@ const chapterTmpl = `
|
||||
}
|
||||
|
||||
// ── mini-player show/hide ─────────────────────────────────────────────────────
|
||||
function showPlayer() { miniPlayer.classList.add('player-visible'); }
|
||||
function hidePlayer() { miniPlayer.classList.remove('player-visible'); }
|
||||
function showPlayer() { /* always visible */ }
|
||||
function hidePlayer() { /* always visible */ }
|
||||
|
||||
// ── scrubber update ───────────────────────────────────────────────────────────
|
||||
function updateScrubber() {
|
||||
@@ -2215,17 +2197,15 @@ const chapterTmpl = `
|
||||
var tot = audio.duration;
|
||||
var pct = (isFinite(tot) && tot > 0) ? Math.min(100, (cur / tot) * 100) : 0;
|
||||
var pctStr = pct.toFixed(1) + '%';
|
||||
// collapsed bar
|
||||
// collapsed bar time
|
||||
playerTimeCur.textContent = fmtTime(cur);
|
||||
playerTimeTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
||||
playerSeekFill.style.width = pctStr;
|
||||
// expanded bar
|
||||
playerFullCur.textContent = fmtTime(cur);
|
||||
playerFullTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
||||
playerFullFill.style.width = pctStr;
|
||||
playerFullThumb.style.left = pctStr;
|
||||
// aria
|
||||
playerSeekTrack.setAttribute('aria-valuenow', Math.round(pct));
|
||||
playerFullTrack.setAttribute('aria-valuenow', Math.round(pct));
|
||||
}
|
||||
|
||||
@@ -2256,7 +2236,6 @@ const chapterTmpl = `
|
||||
document.addEventListener('touchend', function () { dragging = false; });
|
||||
track.addEventListener('click', function (e) { seekFromEvent(track, e); });
|
||||
}
|
||||
attachSeek(playerSeekTrack);
|
||||
attachSeek(playerFullTrack);
|
||||
|
||||
// ── next-chapter prefetch display ────────────────────────────────────────────
|
||||
@@ -2360,33 +2339,26 @@ const chapterTmpl = `
|
||||
|
||||
// ── UI state helpers ──────────────────────────────────────────────────────────
|
||||
function setPlayIcon(icon) {
|
||||
navIcon.innerHTML = icon;
|
||||
playerPlayIcon.innerHTML = icon;
|
||||
}
|
||||
|
||||
function setGenerating() {
|
||||
setPlayIcon('\u231B');
|
||||
navBtn.disabled = true;
|
||||
navBtn.style.opacity = '0.6';
|
||||
playerPlayBtn.disabled = true;
|
||||
voiceSel.disabled = true;
|
||||
speedSlider.disabled = true;
|
||||
setBadge(playerStateBadge, 'generating');
|
||||
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N + '\u00a0— generating\u2026';
|
||||
setStatus('Generating audio\u2026');
|
||||
showPlayer();
|
||||
}
|
||||
function setPlaying() {
|
||||
setPlayIcon('▮▮');
|
||||
navBtn.disabled = false;
|
||||
navBtn.style.opacity = '1';
|
||||
playerPlayBtn.disabled = false;
|
||||
voiceSel.disabled = false;
|
||||
speedSlider.disabled = false;
|
||||
setBadge(playerStateBadge, 'playing');
|
||||
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N;
|
||||
setStatus('');
|
||||
showPlayer();
|
||||
}
|
||||
function setPaused() {
|
||||
setPlayIcon('▶');
|
||||
@@ -2396,8 +2368,6 @@ const chapterTmpl = `
|
||||
function setStopped() {
|
||||
highlightPara(-1);
|
||||
setPlayIcon('▶');
|
||||
navBtn.disabled = false;
|
||||
navBtn.style.opacity = '1';
|
||||
playerPlayBtn.disabled = false;
|
||||
voiceSel.disabled = false;
|
||||
speedSlider.disabled = false;
|
||||
@@ -2405,20 +2375,16 @@ const chapterTmpl = `
|
||||
playerTitle.textContent = '—';
|
||||
setStatus('');
|
||||
updateScrubber();
|
||||
hidePlayer();
|
||||
}
|
||||
function setError(msg) {
|
||||
highlightPara(-1);
|
||||
setPlayIcon('▶');
|
||||
navBtn.disabled = false;
|
||||
navBtn.style.opacity = '1';
|
||||
playerPlayBtn.disabled = false;
|
||||
voiceSel.disabled = false;
|
||||
speedSlider.disabled = false;
|
||||
setBadge(playerStateBadge, 'error');
|
||||
playerTitle.textContent = 'Error';
|
||||
setStatus('Error: ' + msg);
|
||||
showPlayer();
|
||||
}
|
||||
|
||||
// ── server-side audio generation ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user