feat: replace speed slider with preset buttons, replace voice grid with dropdown
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled

This commit is contained in:
Admin
2026-03-02 13:44:06 +05:00
parent 1cdc82249c
commit e0265db7a8

View File

@@ -2332,21 +2332,19 @@ const chapterTmpl = `
hidden
class="fixed inset-x-4 top-1/2 -translate-y-1/2 max-h-[75vh] overflow-y-auto sm:absolute sm:inset-x-auto sm:top-auto sm:translate-y-0 sm:bottom-[calc(100%+0.25rem)] sm:right-[max(0.5rem,calc(50%-32rem+0.5rem))] sm:w-80 sm:max-h-[80vh] bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl z-[100]">
<!-- hidden native select keeps existing JS working unchanged -->
<select id="tts-voice" class="sr-only" aria-hidden="true" tabindex="-1">
{{range .Voices}}
<option value="{{.ID}}"{{if eq .ID $.DefaultVoice}} selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<div class="p-4 space-y-4">
<!-- Speed -->
<label class="block">
<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>
<div>
<span class="block text-xs text-zinc-500 mb-1.5">Speed</span>
<div id="speed-btns" class="flex gap-1.5">
<button type="button" data-speed="1" onclick="selectSpeed(this)" class="speed-btn flex-1 py-1 rounded-lg border text-xs font-medium transition-colors border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100">1×</button>
<button type="button" data-speed="1.25" onclick="selectSpeed(this)" class="speed-btn flex-1 py-1 rounded-lg border text-xs font-medium transition-colors border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100">1.25×</button>
<button type="button" data-speed="1.5" onclick="selectSpeed(this)" class="speed-btn flex-1 py-1 rounded-lg border text-xs font-medium transition-colors border-amber-500 bg-amber-500/10 text-amber-300">1.5×</button>
<button type="button" data-speed="1.75" onclick="selectSpeed(this)" class="speed-btn flex-1 py-1 rounded-lg border text-xs font-medium transition-colors border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100">1.75×</button>
<button type="button" data-speed="2" onclick="selectSpeed(this)" class="speed-btn flex-1 py-1 rounded-lg border text-xs font-medium transition-colors border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100">2×</button>
</div>
</div>
<!-- Toggles -->
<label class="flex items-center justify-between gap-3 cursor-pointer select-none">
@@ -2361,23 +2359,16 @@ const chapterTmpl = `
<!-- Divider -->
<div class="border-t border-zinc-800"></div>
<!-- Voice grid -->
<!-- Voice selector -->
<div>
<span class="block text-xs text-zinc-500 mb-2">Voice</span>
<div id="voice-grid" class="grid grid-cols-2 gap-1.5">
<span class="block text-xs text-zinc-500 mb-1.5">Voice</span>
<select id="tts-voice"
onchange="onVoiceChange(this)"
class="w-full bg-zinc-800 border border-zinc-700 text-zinc-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-amber-500 cursor-pointer">
{{range .Voices}}
<button type="button"
data-voice="{{.ID}}"
onclick="selectVoice(this)"
class="voice-btn flex items-center gap-2 px-2.5 py-1.5 rounded-lg border text-left transition-colors
{{if eq .ID $.DefaultVoice}}border-amber-500 bg-amber-500/10 text-amber-300{{else}}border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100{{end}}">
<span class="flex-1 min-w-0">
<span class="block text-[0.8rem] font-medium leading-tight truncate">{{.Name}}</span>
<span class="block text-[0.65rem] text-zinc-500 leading-tight">{{.Lang}} · {{.Gender}}</span>
</span>
</button>
<option value="{{.ID}}"{{if eq .ID $.DefaultVoice}} selected{{end}}>{{.Name}} ({{.Lang}} · {{.Gender}})</option>
{{end}}
</div>
</select>
</div>
</div>
@@ -2421,8 +2412,7 @@ const chapterTmpl = `
var statusEl = document.getElementById('tts-status');
var statusBar = document.getElementById('tts-status-bar');
var voiceSel = document.getElementById('tts-voice');
var speedSlider = document.getElementById('tts-speed');
var speedLabel = document.getElementById('tts-speed-label');
var speedBtns = document.querySelectorAll('.speed-btn');
var autoplayChk = document.getElementById('tts-autoplay');
var autoscrollChk = document.getElementById('tts-autoscroll');
var article = document.getElementById('chapter-article');
@@ -2537,9 +2527,26 @@ const chapterTmpl = `
// ── localStorage settings ─────────────────────────────────────────────────
var LS_SPEED = 'tts_speed', LS_VOICE = 'tts_voice', LS_AUTONEXT = 'tts_autonext', LS_AUTOSCROLL = 'tts_autoscroll';
function selectSpeed(btn) {
speedBtns.forEach(function (b) {
var active = b === btn;
b.classList.toggle('speed-active', active);
b.classList.toggle('border-amber-500', active);
b.classList.toggle('bg-amber-500/10', active);
b.classList.toggle('text-amber-300', active);
b.classList.toggle('border-zinc-700', !active);
b.classList.toggle('bg-zinc-800', !active);
b.classList.toggle('text-zinc-300', !active);
});
try { localStorage.setItem(LS_SPEED, btn.dataset.speed); } catch(_) {}
prefetchFired = false;
}
window.selectSpeed = selectSpeed;
(function loadSettings() {
var spd = localStorage.getItem(LS_SPEED);
if (spd !== null) { speedSlider.value = spd; speedLabel.textContent = parseFloat(spd).toFixed(1) + '\u00D7'; }
var activeSpd = spd || '1.5';
var btn = document.querySelector('.speed-btn[data-speed="' + activeSpd + '"]');
if (btn) selectSpeed(btn);
var vc = localStorage.getItem(LS_VOICE);
if (vc !== null) { var opt = voiceSel.querySelector('option[value="' + vc + '"]'); if (opt) voiceSel.value = vc; }
var an = localStorage.getItem(LS_AUTONEXT);
@@ -2547,12 +2554,6 @@ const chapterTmpl = `
var as = localStorage.getItem(LS_AUTOSCROLL);
if (as !== null) autoscrollChk.checked = as !== 'false';
})();
speedSlider.addEventListener('input', function () {
if (stale()) return;
speedLabel.textContent = parseFloat(speedSlider.value).toFixed(1) + '\u00D7';
localStorage.setItem(LS_SPEED, speedSlider.value);
prefetchFired = false;
});
voiceSel.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_VOICE, voiceSel.value); prefetchFired = false; });
autoplayChk.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_AUTONEXT, autoplayChk.checked ? 'true' : 'false'); });
autoscrollChk.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_AUTOSCROLL, autoscrollChk.checked ? 'true' : 'false'); });
@@ -2589,11 +2590,15 @@ const chapterTmpl = `
var SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M8 5v14l11-7z"/></svg>';
var SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>';
function setPlayIcon(icon) { playerPlayIcon.innerHTML = icon; }
function getSpeed() {
var b = document.querySelector('.speed-btn.speed-active');
return b ? parseFloat(b.dataset.speed) : 1.5;
}
function setGenerating() {
setPlayIcon('\u231B');
playerPlayBtn.disabled = true;
voiceSel.disabled = true; speedSlider.disabled = true;
voiceSel.disabled = true; speedBtns.forEach(function(b){b.disabled=true;});
setBadge(playerStateBadge, 'generating');
playerStateBadge.hidden = false;
playerTitle.textContent = 'Ch.\u00a0' + CHAPTER_N;
@@ -2604,7 +2609,7 @@ const chapterTmpl = `
if (stale()) return;
setPlayIcon(SVG_PAUSE);
playerPlayBtn.disabled = false;
voiceSel.disabled = false; speedSlider.disabled = false;
voiceSel.disabled = false; speedBtns.forEach(function(b){b.disabled=false;});
setBadge(playerStateBadge, 'playing');
playerStateBadge.hidden = false;
var pct = (audio.duration && isFinite(audio.duration))
@@ -2631,7 +2636,7 @@ const chapterTmpl = `
highlightPara(-1);
setPlayIcon(SVG_PLAY);
playerPlayBtn.disabled = false;
voiceSel.disabled = false; speedSlider.disabled = false;
voiceSel.disabled = false; speedBtns.forEach(function(b){b.disabled=false;});
if (genStartTime > 0) {
var elapsed = Math.round((Date.now() - genStartTime) / 1000);
var label = elapsed >= 60 ? Math.floor(elapsed/60) + 'm ' + (elapsed%60) + 's' : elapsed + 's';
@@ -2652,7 +2657,7 @@ const chapterTmpl = `
highlightPara(-1);
setPlayIcon(SVG_PLAY);
playerPlayBtn.disabled = false;
voiceSel.disabled = false; speedSlider.disabled = false;
voiceSel.disabled = false; speedBtns.forEach(function(b){b.disabled=false;});
setBadge(playerStateBadge, 'error');
playerStateBadge.hidden = false;
playerTitle.textContent = 'Error';
@@ -2679,7 +2684,7 @@ const chapterTmpl = `
fetch('/ui/audio/' + SLUG + '/' + chapterN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ voice: voiceSel.value, speed: parseFloat(speedSlider.value) }),
body: JSON.stringify({ voice: voiceSel.value, speed: getSpeed() }),
signal: ctrl.signal
})
.then(function (res) {
@@ -2714,7 +2719,7 @@ const chapterTmpl = `
fetch('/ui/audio/' + SLUG + '/' + NEXT_N, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ voice: voiceSel.value, speed: parseFloat(speedSlider.value) })
body: JSON.stringify({ voice: voiceSel.value, speed: getSpeed() })
})
.then(function (res) { return res.ok ? res.json() : Promise.reject(res.status); })
.then(function (data) { if (!stale()) { prefetchedUrl = (data && data.url) ? data.url : null; setNextState('ready'); } })
@@ -2842,33 +2847,10 @@ const chapterTmpl = `
}());
}());
// ── Voice card picker ─────────────────────────────────────────────────────
window.selectVoice = function (btn) {
var voiceSel = document.getElementById('tts-voice');
var grid = document.getElementById('voice-grid');
if (!voiceSel || !grid) return;
voiceSel.value = btn.dataset.voice;
try { localStorage.setItem('tts_voice', btn.dataset.voice); } catch(_) {}
grid.querySelectorAll('.voice-btn').forEach(function (b) {
var active = b === btn;
b.classList.toggle('border-amber-500', active);
b.classList.toggle('bg-amber-500/10', active);
b.classList.toggle('text-amber-300', active);
b.classList.toggle('border-zinc-700', !active);
b.classList.toggle('bg-zinc-800', !active);
b.classList.toggle('text-zinc-300', !active);
});
// ── Voice change handler ──────────────────────────────────────────────────
window.onVoiceChange = function (sel) {
try { localStorage.setItem('tts_voice', sel.value); } catch(_) {}
};
(function syncVoiceGrid() {
var voiceSel = document.getElementById('tts-voice');
var grid = document.getElementById('voice-grid');
if (!voiceSel || !grid) return;
var saved = null;
try { saved = localStorage.getItem('tts_voice'); } catch(_) {}
if (!saved) return;
var btn = grid.querySelector('[data-voice="' + saved + '"]');
if (btn) window.selectVoice(btn);
})();
</script>`
func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {