feat: autoplay uses full page navigation, auto-starts TTS on arrival
This commit is contained in:
@@ -2503,11 +2503,6 @@ const chapterTmpl = `
|
|||||||
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 playerStateBadge= document.getElementById('player-state-badge');
|
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 seekTrack = document.getElementById('seek-track');
|
||||||
var seekRail = document.getElementById('seek-rail');
|
var seekRail = document.getElementById('seek-rail');
|
||||||
var seekFill = document.getElementById('seek-fill');
|
var seekFill = document.getElementById('seek-fill');
|
||||||
@@ -2706,7 +2701,6 @@ const chapterTmpl = `
|
|||||||
b.classList.toggle('text-zinc-300', !active);
|
b.classList.toggle('text-zinc-300', !active);
|
||||||
});
|
});
|
||||||
try { localStorage.setItem(LS_SPEED, btn.dataset.speed); } catch(_) {}
|
try { localStorage.setItem(LS_SPEED, btn.dataset.speed); } catch(_) {}
|
||||||
prefetchFired = false;
|
|
||||||
}
|
}
|
||||||
window.selectSpeed = selectSpeed;
|
window.selectSpeed = selectSpeed;
|
||||||
(function loadSettings() {
|
(function loadSettings() {
|
||||||
@@ -2721,7 +2715,7 @@ const chapterTmpl = `
|
|||||||
var as = localStorage.getItem(LS_AUTOSCROLL);
|
var as = localStorage.getItem(LS_AUTOSCROLL);
|
||||||
if (as !== null) autoscrollChk.checked = as !== 'false';
|
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'); });
|
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'); });
|
autoscrollChk.addEventListener('change', function () { if (stale()) return; localStorage.setItem(LS_AUTOSCROLL, autoscrollChk.checked ? 'true' : 'false'); });
|
||||||
|
|
||||||
@@ -2832,66 +2826,13 @@ const chapterTmpl = `
|
|||||||
hideSeek();
|
hideSeek();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── next-chapter prefetch display ─────────────────────────────────────────
|
// ── audio events ──────────────────────────────────────────────────────────
|
||||||
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;
|
|
||||||
|
|
||||||
function onTimeUpdate() {
|
function onTimeUpdate() {
|
||||||
if (stale()) { audio.removeEventListener('timeupdate', onTimeUpdate); return; }
|
if (stale()) { audio.removeEventListener('timeupdate', onTimeUpdate); return; }
|
||||||
updateSeek();
|
updateSeek();
|
||||||
if (audio.duration && isFinite(audio.duration) && !audio.paused) {
|
if (audio.duration && isFinite(audio.duration) && !audio.paused) {
|
||||||
playerStateBadge.textContent = Math.round((audio.currentTime / audio.duration) * 100) + '%';
|
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;
|
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);
|
var idx = Math.min(Math.floor((audio.currentTime / audio.duration) * paras.length), paras.length - 1);
|
||||||
if (!activePara || activePara !== paras[idx]) highlightPara(idx);
|
if (!activePara || activePara !== paras[idx]) highlightPara(idx);
|
||||||
@@ -2905,7 +2846,6 @@ const chapterTmpl = `
|
|||||||
function onEnded() {
|
function onEnded() {
|
||||||
if (stale()) { audio.removeEventListener('ended', onEnded); return; }
|
if (stale()) { audio.removeEventListener('ended', onEnded); return; }
|
||||||
audio.src = '';
|
audio.src = '';
|
||||||
prefetchFired = false;
|
|
||||||
if (autoplayChk.checked && NEXT_N) goNextChapter(); else setStopped();
|
if (autoplayChk.checked && NEXT_N) goNextChapter(); else setStopped();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2921,22 +2861,13 @@ const chapterTmpl = `
|
|||||||
// ── auto-next ─────────────────────────────────────────────────────────────
|
// ── auto-next ─────────────────────────────────────────────────────────────
|
||||||
function goNextChapter() {
|
function goNextChapter() {
|
||||||
if (!NEXT_N) return;
|
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(_) {}
|
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() {
|
function stop() {
|
||||||
if (currentAudioCtrl) { currentAudioCtrl.abort(); currentAudioCtrl = null; }
|
if (currentAudioCtrl) { currentAudioCtrl.abort(); currentAudioCtrl = null; }
|
||||||
audio.pause(); audio.src = '';
|
audio.pause(); audio.src = '';
|
||||||
prefetchFired = false; prefetchedUrl = null;
|
|
||||||
setStopped();
|
setStopped();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3049,7 +2980,7 @@ window.selectVoiceOption = function (btn) {
|
|||||||
}
|
}
|
||||||
if (chevron) chevron.style.transform = '';
|
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'));
|
voiceSel.dispatchEvent(new Event('change'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user