Simplify chapter page audio player UX
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled

- 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:
Admin
2026-03-01 23:56:36 +05:00
parent dfb3f61812
commit 5db21a821a

View File

@@ -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">&#9654;</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">
&#9881;
</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">&#9654;</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">
&#9881;
</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('&#9646;&#9646;');
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('&#9654;');
@@ -2396,8 +2368,6 @@ const chapterTmpl = `
function setStopped() {
highlightPara(-1);
setPlayIcon('&#9654;');
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('&#9654;');
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 ─────────────────────────────────────────────