Pause TTS auto-scroll on user interaction, resume after 3s; add book cover artwork to Media Session
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user