diff --git a/static/css/app.css b/static/css/app.css index b13c066..ae7f56b 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1653,6 +1653,24 @@ body.dnd-mode .timer-display { /* PDF invert */ #reader-overlay.pdf-inverted .pdf-page { filter:invert(1); } +/* ---- Immersive reader mode ---- */ +.navbar { transition: transform 0.35s ease; } +.now-playing-bar{ transition: transform 0.35s ease; } +.reader-overlay { transition: top 0.35s ease, bottom 0.35s ease; } +.reader-header { overflow: hidden; max-height: 60px; + transition: max-height 0.35s ease, padding-top 0.35s ease, padding-bottom 0.35s ease; } + +body.reader-immersive .navbar { transform: translateY(-100%); } +body.reader-immersive .now-playing-bar { transform: translateY(100%); } +body.reader-immersive .reader-overlay { top: 0; bottom: 0; } +body.reader-immersive .reader-header { max-height: 0; padding-top: 0; padding-bottom: 0; } + +body.reader-immersive.reader-show-top .navbar { transform: none; } +body.reader-immersive.reader-show-top .reader-overlay { top: var(--nav-h); } +body.reader-immersive.reader-show-top .reader-header { max-height: 60px; padding-top: 8px; padding-bottom: 8px; } + +body.reader-immersive.reader-show-bottom .now-playing-bar { transform: none; } +body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h); } /* PDF paginated */ .reader-content.pdf-paginated { overflow:hidden !important; display:flex; align-items:center; justify-content:center; } diff --git a/static/js/app.js b/static/js/app.js index 5e81111..944c100 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2911,6 +2911,67 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) { return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages}; } +// --------------------------------------------------------------------------- +// Immersive reader mode — bars slide out after 5 s, return on edge hover/tap +// --------------------------------------------------------------------------- +let _immTimer = null; +let _immTopShowing = false; +let _immBottomShowing = false; +const _IMM_DELAY = 5000; +const _IMM_EDGE = 60; // px from top/bottom edge + +function _immMouseMove(e) { + // When bars are already showing, extend threshold to cover their actual height + const topThreshold = _immTopShowing + ? (document.querySelector('.navbar')?.offsetHeight ?? 0) + + (document.querySelector('.reader-header')?.offsetHeight ?? 0) + 12 + : _IMM_EDGE; + const bottomThreshold = _immBottomShowing + ? (document.querySelector('.now-playing-bar')?.offsetHeight ?? 0) + 12 + : _IMM_EDGE; + + const atTop = e.clientY < topThreshold; + const atBottom = e.clientY > window.innerHeight - bottomThreshold; + if (atTop !== _immTopShowing) { + _immTopShowing = atTop; + document.body.classList.toggle('reader-show-top', atTop); + } + if (atBottom !== _immBottomShowing) { + _immBottomShowing = atBottom; + document.body.classList.toggle('reader-show-bottom', atBottom); + } +} + +function _immTouch(e) { + const y = (e.touches[0] ?? e.changedTouches[0])?.clientY; + if (y == null) return; + if (y < _IMM_EDGE) { + document.body.classList.add('reader-show-top'); + clearTimeout(_immTimer); + _immTimer = setTimeout(() => document.body.classList.remove('reader-show-top'), 3000); + } else if (y > window.innerHeight - _IMM_EDGE) { + document.body.classList.add('reader-show-bottom'); + clearTimeout(_immTimer); + _immTimer = setTimeout(() => document.body.classList.remove('reader-show-bottom'), 3000); + } +} + +function enterReaderImmersiveMode() { + clearTimeout(_immTimer); + _immTimer = setTimeout(() => document.body.classList.add('reader-immersive'), _IMM_DELAY); + window.addEventListener('mousemove', _immMouseMove); + window.addEventListener('touchstart', _immTouch, { passive: true }); +} + +function exitReaderImmersiveMode() { + clearTimeout(_immTimer); + _immTimer = null; + document.body.classList.remove('reader-immersive', 'reader-show-top', 'reader-show-bottom'); + _immTopShowing = false; + _immBottomShowing = false; + window.removeEventListener('mousemove', _immMouseMove); + window.removeEventListener('touchstart', _immTouch); +} async function openBook(bookId) { const overlay = $('reader-overlay'); @@ -3099,6 +3160,8 @@ async function openBook(bookId) { } } + enterReaderImmersiveMode(); + } catch (e) { overlay.style.display = 'none'; alert(`Failed to open book: ${e.message}`); @@ -3188,6 +3251,7 @@ async function saveReaderProgress() { } function closeReader() { + exitReaderImmersiveMode(); // Save progress BEFORE hiding — scrollHeight/clientHeight return 0 once display:none saveReaderProgress(); if (bookmarksDirty) saveBookmarks();