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 PREV_N = {{.PrevN}};
|
||||||
var SLUG = '{{.Slug}}';
|
var SLUG = '{{.Slug}}';
|
||||||
var CHAPTER_N = {{.ChapterN}};
|
var CHAPTER_N = {{.ChapterN}};
|
||||||
|
var COVER_URL = '{{.Cover}}';
|
||||||
|
|
||||||
// ── reading progress ─────────────────────────────────────────────────────────
|
// ── reading progress ─────────────────────────────────────────────────────────
|
||||||
(function saveProgress() {
|
(function saveProgress() {
|
||||||
@@ -2268,12 +2269,27 @@ const chapterTmpl = `
|
|||||||
var paras = Array.prototype.slice.call((article || {querySelectorAll: function(){return[];}}).querySelectorAll('p'));
|
var paras = Array.prototype.slice.call((article || {querySelectorAll: function(){return[];}}).querySelectorAll('p'));
|
||||||
var activePara = null;
|
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) {
|
function highlightPara(idx) {
|
||||||
if (activePara) activePara.classList.remove('tts-active');
|
if (activePara) activePara.classList.remove('tts-active');
|
||||||
activePara = (idx >= 0 && idx < paras.length) ? paras[idx] : null;
|
activePara = (idx >= 0 && idx < paras.length) ? paras[idx] : null;
|
||||||
if (activePara) {
|
if (activePara) {
|
||||||
activePara.classList.add('tts-active');
|
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({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: 'Ch.\u00a0' + CHAPTER_N,
|
title: 'Ch.\u00a0' + CHAPTER_N,
|
||||||
artist: SLUG.replace(/-/g, ' '),
|
artist: SLUG.replace(/-/g, ' '),
|
||||||
album: 'libnovel'
|
album: 'libnovel',
|
||||||
|
artwork: COVER_URL ? [{ src: COVER_URL, sizes: '512x512', type: 'image/jpeg' }] : []
|
||||||
});
|
});
|
||||||
navigator.mediaSession.setActionHandler('play', function() {
|
navigator.mediaSession.setActionHandler('play', function() {
|
||||||
audio.play().then(setPlaying).catch(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))
|
title := firstHeading(raw, fmt.Sprintf("Chapter %d", n))
|
||||||
chapterTitle, chapterDate := writer.SplitChapterTitle(title)
|
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))
|
t := template.Must(template.New("chapter").Parse(chapterTmpl))
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
_ = t.Execute(&buf, struct {
|
_ = t.Execute(&buf, struct {
|
||||||
@@ -2606,6 +2629,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
|||||||
AllChapters interface{}
|
AllChapters interface{}
|
||||||
Voices []string
|
Voices []string
|
||||||
DefaultVoice string
|
DefaultVoice string
|
||||||
|
Cover string
|
||||||
}{
|
}{
|
||||||
Slug: slug,
|
Slug: slug,
|
||||||
HTML: template.HTML(htmlBuf.String()),
|
HTML: template.HTML(htmlBuf.String()),
|
||||||
@@ -2617,6 +2641,7 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
|
|||||||
AllChapters: chapters,
|
AllChapters: chapters,
|
||||||
Voices: kokoroVoices,
|
Voices: kokoroVoices,
|
||||||
DefaultVoice: s.kokoroVoice,
|
DefaultVoice: s.kokoroVoice,
|
||||||
|
Cover: coverURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
s.respond(w, r, chapterTitle, buf.String())
|
s.respond(w, r, chapterTitle, buf.String())
|
||||||
|
|||||||
Reference in New Issue
Block a user