Pause TTS auto-scroll on user interaction, resume after 3s; add book cover artwork to Media Session
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 10:06:59 +05:00
parent 090d155c19
commit 427e952253

View File

@@ -2083,6 +2083,7 @@ const chapterTmpl = `
var PREV_N = {{.PrevN}};
var SLUG = '{{.Slug}}';
var CHAPTER_N = {{.ChapterN}};
var COVER_URL = '{{.Cover}}';
// ── reading progress ─────────────────────────────────────────────────────────
(function saveProgress() {
@@ -2268,12 +2269,27 @@ const chapterTmpl = `
var paras = Array.prototype.slice.call((article || {querySelectorAll: function(){return[];}}).querySelectorAll('p'));
var activePara = null;
// ── auto-scroll pause on user interaction ────────────────────────────────────
var autoScrollEnabled = true;
var autoScrollTimer = null;
function resetAutoScrollTimer() {
autoScrollEnabled = false;
clearTimeout(autoScrollTimer);
autoScrollTimer = setTimeout(function () { autoScrollEnabled = true; }, 3000);
}
document.addEventListener('wheel', resetAutoScrollTimer, { passive: true });
document.addEventListener('touchmove', resetAutoScrollTimer, { passive: true });
document.addEventListener('keydown', function (e) {
var scrollKeys = { ArrowUp: 1, ArrowDown: 1, PageUp: 1, PageDown: 1, Home: 1, End: 1, ' ': 1 };
if (scrollKeys[e.key]) resetAutoScrollTimer();
});
function highlightPara(idx) {
if (activePara) activePara.classList.remove('tts-active');
activePara = (idx >= 0 && idx < paras.length) ? paras[idx] : null;
if (activePara) {
activePara.classList.add('tts-active');
activePara.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
if (autoScrollEnabled) activePara.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
@@ -2460,7 +2476,8 @@ const chapterTmpl = `
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Ch.\u00a0' + CHAPTER_N,
artist: SLUG.replace(/-/g, ' '),
album: 'libnovel'
album: 'libnovel',
artwork: COVER_URL ? [{ src: COVER_URL, sizes: '512x512', type: 'image/jpeg' }] : []
});
navigator.mediaSession.setActionHandler('play', function() {
audio.play().then(setPlaying).catch(function(){});
@@ -2593,6 +2610,12 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
title := firstHeading(raw, fmt.Sprintf("Chapter %d", n))
chapterTitle, chapterDate := writer.SplitChapterTitle(title)
// Load cover URL for Media Session artwork (best-effort; ignore errors).
var coverURL string
if meta, ok, err := s.writer.ReadMetadata(slug); err == nil && ok {
coverURL = meta.Cover
}
t := template.Must(template.New("chapter").Parse(chapterTmpl))
var buf bytes.Buffer
_ = t.Execute(&buf, struct {
@@ -2606,6 +2629,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
AllChapters interface{}
Voices []string
DefaultVoice string
Cover string
}{
Slug: slug,
HTML: template.HTML(htmlBuf.String()),
@@ -2617,6 +2641,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
AllChapters: chapters,
Voices: kokoroVoices,
DefaultVoice: s.kokoroVoice,
Cover: coverURL,
})
s.respond(w, r, chapterTitle, buf.String())