feat: autoplay uses full page navigation, auto-starts TTS on arrival
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 14:19:21 +05:00
parent 412b0fb478
commit 90a16a7223

View File

@@ -2503,11 +2503,6 @@ const chapterTmpl = `
var playerPlayIcon = document.getElementById('player-play-icon');
var playerTitle = document.getElementById('player-title');
var playerStateBadge= document.getElementById('player-state-badge');
var playerNextRow = document.getElementById('player-next-row');
var playerNextBadge = document.getElementById('player-next-badge');
var settingsPanel = document.getElementById('settings-panel');
var chapterListPanel= document.getElementById('chapter-list-panel');
var chapterListBtn = document.getElementById('chapter-list-btn');
var seekTrack = document.getElementById('seek-track');
var seekRail = document.getElementById('seek-rail');
var seekFill = document.getElementById('seek-fill');
@@ -2706,7 +2701,6 @@ const chapterTmpl = `
b.classList.toggle('text-zinc-300', !active);
});
try { localStorage.setItem(LS_SPEED, btn.dataset.speed); } catch(_) {}
prefetchFired = false;
}
window.selectSpeed = selectSpeed;
(function loadSettings() {
@@ -2721,7 +2715,7 @@ const chapterTmpl = `
var as = localStorage.getItem(LS_AUTOSCROLL);
if (as !== null) autoscrollChk.checked = as !== 'false';
})();
voiceSel.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_VOICE, voiceSel.value); prefetchFired = false; });
voiceSel.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_VOICE, voiceSel.value); });
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'); });
@@ -2832,66 +2826,13 @@ const chapterTmpl = `
hideSeek();
}
// ── next-chapter prefetch display ─────────────────────────────────────────
function setNextState(state) {
if (!NEXT_N) { playerNextRow.hidden = true; return; }
playerNextRow.hidden = false;
setBadge(playerNextBadge, state);
}
// ── server-side audio generation ──────────────────────────────────────────
var currentAudioCtrl = null;
var genStartTime = 0;
function generateAudio(chapterN, cb) {
if (currentAudioCtrl) currentAudioCtrl.abort();
var ctrl = new AbortController();
currentAudioCtrl = ctrl;
genStartTime = Date.now();
fetch('/ui/audio/' + SLUG + '/' + chapterN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ voice: voiceSel.value, speed: getSpeed() }),
signal: ctrl.signal
})
.then(function (res) {
if (!res.ok) return res.text().then(function (t) { throw new Error(res.status + ': ' + t); });
return res.json();
})
.then(function (data) {
if (stale()) return;
if (!data || !data.url) throw new Error('no url in response');
cb(data.url);
})
.catch(function (e) {
if (e.name === 'AbortError' || stale()) return;
setError(e.message);
});
}
// ── next-chapter prefetch at 80% ──────────────────────────────────────────
var prefetchFired = false;
var prefetchedUrl = null;
// ── audio events ──────────────────────────────────────────────────────────
function onTimeUpdate() {
if (stale()) { audio.removeEventListener('timeupdate', onTimeUpdate); return; }
updateSeek();
if (audio.duration && isFinite(audio.duration) && !audio.paused) {
playerStateBadge.textContent = Math.round((audio.currentTime / audio.duration) * 100) + '%';
}
if (NEXT_N && !prefetchFired && audio.duration && isFinite(audio.duration) &&
audio.currentTime / audio.duration >= 0.8) {
prefetchFired = true;
setNextState('generating');
fetch('/ui/audio/' + SLUG + '/' + NEXT_N, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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'); } })
.catch(function () { if (!stale()) setNextState('error'); });
}
if (paras.length === 0 || !audio.duration || !isFinite(audio.duration)) return;
var idx = Math.min(Math.floor((audio.currentTime / audio.duration) * paras.length), paras.length - 1);
if (!activePara || activePara !== paras[idx]) highlightPara(idx);
@@ -2905,7 +2846,6 @@ const chapterTmpl = `
function onEnded() {
if (stale()) { audio.removeEventListener('ended', onEnded); return; }
audio.src = '';
prefetchFired = false;
if (autoplayChk.checked && NEXT_N) goNextChapter(); else setStopped();
}
@@ -2921,22 +2861,13 @@ const chapterTmpl = `
// ── auto-next ─────────────────────────────────────────────────────────────
function goNextChapter() {
if (!NEXT_N) return;
if (prefetchedUrl) {
var url = prefetchedUrl; prefetchedUrl = null;
audio.src = url; audio.load();
if ('mediaSession' in navigator && navigator.mediaSession.metadata)
navigator.mediaSession.metadata.title = 'Ch.\u00a0' + NEXT_N;
return;
}
var nextURL = '/books/' + SLUG + '/chapters/' + NEXT_N;
try { localStorage.setItem('tts_autostart', '1'); } catch(_) {}
htmx.ajax('GET', nextURL, { target: '#main-content', swap: 'innerHTML', pushURL: nextURL });
window.location.href = '/books/' + SLUG + '/chapters/' + NEXT_N;
}
function stop() {
if (currentAudioCtrl) { currentAudioCtrl.abort(); currentAudioCtrl = null; }
audio.pause(); audio.src = '';
prefetchFired = false; prefetchedUrl = null;
setStopped();
}
@@ -3049,7 +2980,7 @@ window.selectVoiceOption = function (btn) {
}
if (chevron) chevron.style.transform = '';
// fire change event so existing listener persists prefetchFired reset
// fire change event so existing voice listener persists
voiceSel.dispatchEvent(new Event('change'));
};