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>
|
</a>
|
||||||
{{end}}
|
{{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>
|
</div>
|
||||||
|
|
||||||
<!-- TTS status strip -->
|
<!-- TTS status strip -->
|
||||||
@@ -1937,31 +1919,6 @@ const chapterTmpl = `
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -2001,10 +1958,37 @@ const chapterTmpl = `
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- ─── Mini audio player (slides up from bottom when audio starts) ────────── -->
|
<!-- ─── Mini audio player (always visible at bottom) ────────────────────── -->
|
||||||
<aside id="mini-player"
|
<aside id="mini-player"
|
||||||
aria-label="Audio 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 -->
|
<!-- Collapsed bar: play/pause · title · time · expand -->
|
||||||
<div id="player-bar"
|
<div id="player-bar"
|
||||||
@@ -2019,15 +2003,9 @@ const chapterTmpl = `
|
|||||||
<span id="player-play-icon" aria-hidden="true">▶</span>
|
<span id="player-play-icon" aria-hidden="true">▶</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Chapter title + inline mini progress -->
|
<!-- Chapter title -->
|
||||||
<div class="flex-1 min-w-0 flex flex-col justify-center gap-0.5">
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Time display -->
|
<!-- Time display -->
|
||||||
@@ -2036,6 +2014,16 @@ const chapterTmpl = `
|
|||||||
<span id="player-time-tot" class="text-zinc-600">0:00</span>
|
<span id="player-time-tot" class="text-zinc-600">0:00</span>
|
||||||
</div>
|
</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 -->
|
<!-- Expand toggle -->
|
||||||
<button id="player-expand-btn"
|
<button id="player-expand-btn"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2124,12 +2112,10 @@ const chapterTmpl = `
|
|||||||
.queue-badge-paused { background: #1c1917; color: #d6d3d1; }
|
.queue-badge-paused { background: #1c1917; color: #d6d3d1; }
|
||||||
.queue-badge-error { background: #450a0a; color: #f87171; }
|
.queue-badge-error { background: #450a0a; color: #f87171; }
|
||||||
|
|
||||||
/* Mini-player slide-up */
|
/* Mini-player is always visible on chapter pages */
|
||||||
#mini-player { will-change: transform; }
|
|
||||||
#mini-player.player-visible { transform: translateY(0); }
|
|
||||||
|
|
||||||
/* Seek track hit-area padding for easier touch */
|
/* Seek track hit-area padding for easier touch */
|
||||||
#player-seek-track, #player-full-track {
|
#player-full-track {
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
margin-top: -6px;
|
margin-top: -6px;
|
||||||
@@ -2156,8 +2142,6 @@ const chapterTmpl = `
|
|||||||
|
|
||||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
var audio = document.getElementById('tts-audio');
|
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 statusEl = document.getElementById('tts-status');
|
||||||
var statusBar = document.getElementById('tts-status-bar');
|
var statusBar = document.getElementById('tts-status-bar');
|
||||||
var voiceSel = document.getElementById('tts-voice');
|
var voiceSel = document.getElementById('tts-voice');
|
||||||
@@ -2171,8 +2155,6 @@ const chapterTmpl = `
|
|||||||
var playerPlayBtn = document.getElementById('player-play-btn');
|
var playerPlayBtn = document.getElementById('player-play-btn');
|
||||||
var playerPlayIcon = document.getElementById('player-play-icon');
|
var playerPlayIcon = document.getElementById('player-play-icon');
|
||||||
var playerTitle = document.getElementById('player-title');
|
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 playerTimeCur = document.getElementById('player-time-cur');
|
||||||
var playerTimeTot = document.getElementById('player-time-tot');
|
var playerTimeTot = document.getElementById('player-time-tot');
|
||||||
var playerExpandBtn = document.getElementById('player-expand-btn');
|
var playerExpandBtn = document.getElementById('player-expand-btn');
|
||||||
@@ -2206,8 +2188,8 @@ const chapterTmpl = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── mini-player show/hide ─────────────────────────────────────────────────────
|
// ── mini-player show/hide ─────────────────────────────────────────────────────
|
||||||
function showPlayer() { miniPlayer.classList.add('player-visible'); }
|
function showPlayer() { /* always visible */ }
|
||||||
function hidePlayer() { miniPlayer.classList.remove('player-visible'); }
|
function hidePlayer() { /* always visible */ }
|
||||||
|
|
||||||
// ── scrubber update ───────────────────────────────────────────────────────────
|
// ── scrubber update ───────────────────────────────────────────────────────────
|
||||||
function updateScrubber() {
|
function updateScrubber() {
|
||||||
@@ -2215,17 +2197,15 @@ const chapterTmpl = `
|
|||||||
var tot = audio.duration;
|
var tot = audio.duration;
|
||||||
var pct = (isFinite(tot) && tot > 0) ? Math.min(100, (cur / tot) * 100) : 0;
|
var pct = (isFinite(tot) && tot > 0) ? Math.min(100, (cur / tot) * 100) : 0;
|
||||||
var pctStr = pct.toFixed(1) + '%';
|
var pctStr = pct.toFixed(1) + '%';
|
||||||
// collapsed bar
|
// collapsed bar time
|
||||||
playerTimeCur.textContent = fmtTime(cur);
|
playerTimeCur.textContent = fmtTime(cur);
|
||||||
playerTimeTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
playerTimeTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
||||||
playerSeekFill.style.width = pctStr;
|
|
||||||
// expanded bar
|
// expanded bar
|
||||||
playerFullCur.textContent = fmtTime(cur);
|
playerFullCur.textContent = fmtTime(cur);
|
||||||
playerFullTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
playerFullTot.textContent = isFinite(tot) ? fmtTime(tot) : '0:00';
|
||||||
playerFullFill.style.width = pctStr;
|
playerFullFill.style.width = pctStr;
|
||||||
playerFullThumb.style.left = pctStr;
|
playerFullThumb.style.left = pctStr;
|
||||||
// aria
|
// aria
|
||||||
playerSeekTrack.setAttribute('aria-valuenow', Math.round(pct));
|
|
||||||
playerFullTrack.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; });
|
document.addEventListener('touchend', function () { dragging = false; });
|
||||||
track.addEventListener('click', function (e) { seekFromEvent(track, e); });
|
track.addEventListener('click', function (e) { seekFromEvent(track, e); });
|
||||||
}
|
}
|
||||||
attachSeek(playerSeekTrack);
|
|
||||||
attachSeek(playerFullTrack);
|
attachSeek(playerFullTrack);
|
||||||
|
|
||||||
// ── next-chapter prefetch display ────────────────────────────────────────────
|
// ── next-chapter prefetch display ────────────────────────────────────────────
|
||||||
@@ -2360,33 +2339,26 @@ const chapterTmpl = `
|
|||||||
|
|
||||||
// ── UI state helpers ──────────────────────────────────────────────────────────
|
// ── UI state helpers ──────────────────────────────────────────────────────────
|
||||||
function setPlayIcon(icon) {
|
function setPlayIcon(icon) {
|
||||||
navIcon.innerHTML = icon;
|
|
||||||
playerPlayIcon.innerHTML = icon;
|
playerPlayIcon.innerHTML = icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGenerating() {
|
function setGenerating() {
|
||||||
setPlayIcon('\u231B');
|
setPlayIcon('\u231B');
|
||||||
navBtn.disabled = true;
|
|
||||||
navBtn.style.opacity = '0.6';
|
|
||||||
playerPlayBtn.disabled = true;
|
playerPlayBtn.disabled = true;
|
||||||
voiceSel.disabled = true;
|
voiceSel.disabled = true;
|
||||||
speedSlider.disabled = true;
|
speedSlider.disabled = true;
|
||||||
setBadge(playerStateBadge, 'generating');
|
setBadge(playerStateBadge, 'generating');
|
||||||
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N + '\u00a0— generating\u2026';
|
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N + '\u00a0— generating\u2026';
|
||||||
setStatus('Generating audio\u2026');
|
setStatus('Generating audio\u2026');
|
||||||
showPlayer();
|
|
||||||
}
|
}
|
||||||
function setPlaying() {
|
function setPlaying() {
|
||||||
setPlayIcon('▮▮');
|
setPlayIcon('▮▮');
|
||||||
navBtn.disabled = false;
|
|
||||||
navBtn.style.opacity = '1';
|
|
||||||
playerPlayBtn.disabled = false;
|
playerPlayBtn.disabled = false;
|
||||||
voiceSel.disabled = false;
|
voiceSel.disabled = false;
|
||||||
speedSlider.disabled = false;
|
speedSlider.disabled = false;
|
||||||
setBadge(playerStateBadge, 'playing');
|
setBadge(playerStateBadge, 'playing');
|
||||||
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N;
|
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N;
|
||||||
setStatus('');
|
setStatus('');
|
||||||
showPlayer();
|
|
||||||
}
|
}
|
||||||
function setPaused() {
|
function setPaused() {
|
||||||
setPlayIcon('▶');
|
setPlayIcon('▶');
|
||||||
@@ -2396,8 +2368,6 @@ const chapterTmpl = `
|
|||||||
function setStopped() {
|
function setStopped() {
|
||||||
highlightPara(-1);
|
highlightPara(-1);
|
||||||
setPlayIcon('▶');
|
setPlayIcon('▶');
|
||||||
navBtn.disabled = false;
|
|
||||||
navBtn.style.opacity = '1';
|
|
||||||
playerPlayBtn.disabled = false;
|
playerPlayBtn.disabled = false;
|
||||||
voiceSel.disabled = false;
|
voiceSel.disabled = false;
|
||||||
speedSlider.disabled = false;
|
speedSlider.disabled = false;
|
||||||
@@ -2405,20 +2375,16 @@ const chapterTmpl = `
|
|||||||
playerTitle.textContent = '—';
|
playerTitle.textContent = '—';
|
||||||
setStatus('');
|
setStatus('');
|
||||||
updateScrubber();
|
updateScrubber();
|
||||||
hidePlayer();
|
|
||||||
}
|
}
|
||||||
function setError(msg) {
|
function setError(msg) {
|
||||||
highlightPara(-1);
|
highlightPara(-1);
|
||||||
setPlayIcon('▶');
|
setPlayIcon('▶');
|
||||||
navBtn.disabled = false;
|
|
||||||
navBtn.style.opacity = '1';
|
|
||||||
playerPlayBtn.disabled = false;
|
playerPlayBtn.disabled = false;
|
||||||
voiceSel.disabled = false;
|
voiceSel.disabled = false;
|
||||||
speedSlider.disabled = false;
|
speedSlider.disabled = false;
|
||||||
setBadge(playerStateBadge, 'error');
|
setBadge(playerStateBadge, 'error');
|
||||||
playerTitle.textContent = 'Error';
|
playerTitle.textContent = 'Error';
|
||||||
setStatus('Error: ' + msg);
|
setStatus('Error: ' + msg);
|
||||||
showPlayer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── server-side audio generation ─────────────────────────────────────────────
|
// ── server-side audio generation ─────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user