feat(reader): continuous reading mode, seamless infinite scroll, and end-of-chapter nav cards
This commit is contained in:
@@ -146,6 +146,12 @@
|
|||||||
- Сохранение не только пикселей `scrollTop`, но и индекса первого видимого абзаца (`anchor_paragraph_idx`).
|
- Сохранение не только пикселей `scrollTop`, но и индекса первого видимого абзаца (`anchor_paragraph_idx`).
|
||||||
- Устойчивость к изменению шрифта `Aa` и ориентации экрана: восстановление скролла к конкретному предложению/абзацу, а не слепым пикселям.
|
- Устойчивость к изменению шрифта `Aa` и ориентации экрана: восстановление скролла к конкретному предложению/абзацу, а не слепым пикселям.
|
||||||
- Фиксация скролл-контейнера `#reader-scroll-area` с аппаратным ускорением на мобильных.
|
- Фиксация скролл-контейнера `#reader-scroll-area` с аппаратным ускорением на мобильных.
|
||||||
|
- [x] **6.5. Сквозное чтение и бесшовная автоподгрузка (Continuous Book View & Seamless Infinite Append)**:
|
||||||
|
- Режим «Вся книга целиком» (`flowMode: 'continuous'`): объединение всех глав книги в один непрерывный свиток со стильными разделителями глав без необходимости перелистывания.
|
||||||
|
- Режим «По главам» с автоподгрузкой (`autoLoadNext: true`): бесшовное присоединение следующей главы снизу при приближении к концу текущей главы.
|
||||||
|
- Навигационный блок завершения главы: эргономичные кнопки «◀ Назад» и «Следующая: [Название] ▶» прямо под текстом (без необходимости тянуться наверх).
|
||||||
|
- Настройка режима чтения («По главам» / «Вся книга») и переключатель автоподгрузки добавлены в панель настроек `Aa` с автосохранением в `localStorage`.
|
||||||
|
- Карточка завершения книги (`🎉 Книга прочитана!`) с быстрыми ссылками на тренировку слов и возврат в библиотеку.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3875,5 +3875,151 @@ body {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Continuous Reading & Chapter End Navigation Card
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* Chapter Divider (Between chapters in continuous / autoload mode) */
|
||||||
|
.reader-chapter-divider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 56px 0 36px;
|
||||||
|
padding: 24px 16px;
|
||||||
|
border-top: 1px dashed var(--border-color);
|
||||||
|
border-bottom: 1px dashed var(--border-color);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
scroll-margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-divider .divider-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--bg-input);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-divider .divider-title {
|
||||||
|
font-size: 22px !important;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 0 !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chapter End Navigation Card */
|
||||||
|
.reader-chapter-footer {
|
||||||
|
margin: 48px 0 64px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 14px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer .chapter-footer-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer .chapter-footer-status .status-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: var(--success);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer .chapter-footer-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer .reader-footer-nav-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
min-height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer .chapter-footer-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Book Completion Card */
|
||||||
|
.reader-chapter-footer.reader-book-finished {
|
||||||
|
background: linear-gradient(180deg, rgba(56, 189, 248, 0.08) 0%, rgba(20, 24, 32, 0.95) 100%);
|
||||||
|
border-color: rgba(56, 189, 248, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-chapter-footer.reader-book-finished .chapter-footer-hint {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reading Theme Adaptations */
|
||||||
|
.reader-scroll-area.reader-theme-sepia .reader-chapter-divider,
|
||||||
|
.reader-scroll-area.reader-theme-light .reader-chapter-divider {
|
||||||
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-sepia .reader-chapter-footer,
|
||||||
|
.reader-scroll-area.reader-theme-light .reader-chapter-footer {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-oled .reader-chapter-divider {
|
||||||
|
background: #080808;
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-scroll-area.reader-theme-oled .reader-chapter-footer {
|
||||||
|
background: #0c0c0c;
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
|||||||
+20
-2
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Notes — Персональный тренажер и словарь английского языка</title>
|
<title>Notes — Персональный тренажер и словарь английского языка</title>
|
||||||
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
|
<meta name="description" content="Защищённый персональный тренажёр и словарь английского языка с интервальными повторениями SRS SM-2, контекстным поиском и озвучкой">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=3.5">
|
<link rel="stylesheet" href="/static/css/style.css?v=3.6">
|
||||||
|
|
||||||
<!-- PWA meta and icons -->
|
<!-- PWA meta and icons -->
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
@@ -965,6 +965,24 @@
|
|||||||
<button id="reader-settings-close-btn" class="popup-close-btn">✕</button>
|
<button id="reader-settings-close-btn" class="popup-close-btn">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="reader-settings-body">
|
<div class="reader-settings-body">
|
||||||
|
<!-- Reading Flow Mode -->
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">Режим чтения</span>
|
||||||
|
<div class="setting-segmented-control" id="reader-mode-group">
|
||||||
|
<button type="button" class="seg-btn active" data-flow="chapters">📖 По главам</button>
|
||||||
|
<button type="button" class="seg-btn" data-flow="continuous">📜 Вся книга</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Autoload next chapter toggle (active in chapters mode) -->
|
||||||
|
<div class="setting-row" id="setting-autoload-row">
|
||||||
|
<span class="setting-label">Переход между главами</span>
|
||||||
|
<div class="setting-segmented-control" id="reader-autoload-group">
|
||||||
|
<button type="button" class="seg-btn active" data-autoload="true">⚡ Автоподгрузка</button>
|
||||||
|
<button type="button" class="seg-btn" data-autoload="false">🔘 По кнопке</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Font Family -->
|
<!-- Font Family -->
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<span class="setting-label">Шрифт</span>
|
<span class="setting-label">Шрифт</span>
|
||||||
@@ -1056,7 +1074,7 @@
|
|||||||
<!-- Audio Element for native streaming -->
|
<!-- Audio Element for native streaming -->
|
||||||
<audio id="global-audio-player" preload="none"></audio>
|
<audio id="global-audio-player" preload="none"></audio>
|
||||||
|
|
||||||
<script src="/static/js/app.js?v=3.5"></script>
|
<script src="/static/js/app.js?v=3.6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+451
-54
@@ -113,6 +113,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const readerMarginsGroup = document.getElementById('reader-margins-group');
|
const readerMarginsGroup = document.getElementById('reader-margins-group');
|
||||||
const readerThemeGroup = document.getElementById('reader-theme-group');
|
const readerThemeGroup = document.getElementById('reader-theme-group');
|
||||||
const readerAlignGroup = document.getElementById('reader-align-group');
|
const readerAlignGroup = document.getElementById('reader-align-group');
|
||||||
|
const readerModeGroup = document.getElementById('reader-mode-group');
|
||||||
|
const readerAutoloadGroup = document.getElementById('reader-autoload-group');
|
||||||
|
const settingAutoloadRow = document.getElementById('setting-autoload-row');
|
||||||
|
|
||||||
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
|
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
|
||||||
const readerTrainBtn = document.getElementById('reader-train-btn');
|
const readerTrainBtn = document.getElementById('reader-train-btn');
|
||||||
@@ -143,11 +146,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
lineHeight: 1.65,
|
lineHeight: 1.65,
|
||||||
marginWidth: 'normal',
|
marginWidth: 'normal',
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
textAlign: 'left'
|
textAlign: 'left',
|
||||||
|
flowMode: 'chapters', // 'chapters' | 'continuous'
|
||||||
|
autoLoadNext: true // boolean: seamlessly load next chapter on scroll
|
||||||
};
|
};
|
||||||
let readerSettings = { ...DEFAULT_READER_SETTINGS };
|
let readerSettings = { ...DEFAULT_READER_SETTINGS };
|
||||||
let currentBook = null;
|
let currentBook = null;
|
||||||
let currentChapterIndex = 0;
|
let currentChapterIndex = 0;
|
||||||
|
const loadedChaptersSet = new Set();
|
||||||
|
let isAppendingChapter = false;
|
||||||
let isHighlightEnabled = false;
|
let isHighlightEnabled = false;
|
||||||
let popupCurrentData = null;
|
let popupCurrentData = null;
|
||||||
|
|
||||||
@@ -2127,7 +2134,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (currentBook && currentBook.id === bookId) {
|
if (currentBook && currentBook.id === bookId) {
|
||||||
if (specificChapterIdx !== null && specificChapterIdx !== undefined && !isNaN(specificChapterIdx)) {
|
if (specificChapterIdx !== null && specificChapterIdx !== undefined && !isNaN(specificChapterIdx)) {
|
||||||
if (specificChapterIdx !== currentChapterIndex) {
|
if (specificChapterIdx !== currentChapterIndex) {
|
||||||
renderChapter(specificChapterIdx, 0, 0, 0);
|
if (readerSettings.flowMode === 'continuous' || loadedChaptersSet.has(specificChapterIdx)) {
|
||||||
|
currentChapterIndex = specificChapterIdx;
|
||||||
|
if (readerChapterSelect) readerChapterSelect.value = currentChapterIndex;
|
||||||
|
const target = document.getElementById(`chapter-anchor-${specificChapterIdx}`)
|
||||||
|
|| readerContentBody.querySelector(`[data-chapter-idx="${specificChapterIdx}"]`);
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderChapter(specificChapterIdx, 0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updateHash) {
|
if (updateHash) {
|
||||||
@@ -2213,40 +2230,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChapter(index, restoreScrollTop = null, restoreFraction = null, restoreAnchorIdx = null) {
|
function buildChapterDOM(chapterIdx, knownWordsMap, isContinuous = false) {
|
||||||
if (!currentBook || !currentBook.chapters) return;
|
const chapter = currentBook.chapters[chapterIdx] || { title: '', content: '' };
|
||||||
currentChapterIndex = Math.max(0, Math.min(index, currentBook.chapters.length - 1));
|
const frag = document.createDocumentFragment();
|
||||||
readerChapterSelect.value = currentChapterIndex;
|
|
||||||
|
|
||||||
// Refresh status icons in chapter select
|
if (chapter.title || isContinuous) {
|
||||||
if (readerChapterSelect.options) {
|
if (isContinuous || chapterIdx > 0) {
|
||||||
for (let i = 0; i < readerChapterSelect.options.length; i++) {
|
const divider = document.createElement('div');
|
||||||
const rawTitle = currentBook.chapters[i]?.title || `Глава ${i + 1}`;
|
divider.className = 'reader-chapter-divider';
|
||||||
const icon = i < currentChapterIndex ? '✔ ' : (i === currentChapterIndex ? '▶ ' : '○ ');
|
divider.id = `chapter-anchor-${chapterIdx}`;
|
||||||
readerChapterSelect.options[i].textContent = `${icon}${rawTitle}`;
|
divider.setAttribute('data-chapter-idx', chapterIdx.toString());
|
||||||
|
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'divider-badge';
|
||||||
|
badge.textContent = `Глава ${chapterIdx + 1} из ${currentBook.chapters.length}`;
|
||||||
|
|
||||||
|
const titleEl = document.createElement('h2');
|
||||||
|
titleEl.className = 'divider-title';
|
||||||
|
titleEl.textContent = chapter.title || `Глава ${chapterIdx + 1}`;
|
||||||
|
|
||||||
|
divider.appendChild(badge);
|
||||||
|
divider.appendChild(titleEl);
|
||||||
|
frag.appendChild(divider);
|
||||||
|
} else if (chapter.title) {
|
||||||
|
const h = document.createElement('h2');
|
||||||
|
h.textContent = chapter.title;
|
||||||
|
h.setAttribute('data-block-idx', '0');
|
||||||
|
h.setAttribute('data-chapter-idx', chapterIdx.toString());
|
||||||
|
h.id = `chapter-anchor-${chapterIdx}`;
|
||||||
|
frag.appendChild(h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
|
||||||
readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
|
||||||
|
|
||||||
const chapter = currentBook.chapters[currentChapterIndex] || { title: '', content: '' };
|
|
||||||
readerContentBody.innerHTML = '';
|
|
||||||
|
|
||||||
if (chapter.title) {
|
|
||||||
const h = document.createElement('h2');
|
|
||||||
h.textContent = chapter.title;
|
|
||||||
h.setAttribute('data-block-idx', '0');
|
|
||||||
readerContentBody.appendChild(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map existing user vocabulary for status marking
|
|
||||||
const knownWordsMap = new Map();
|
|
||||||
allCards.forEach(c => {
|
|
||||||
if (c.term) knownWordsMap.set(c.term.trim().toLowerCase(), c);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse content into structured blocks (code, headings, lists, quotes, paragraphs)
|
|
||||||
const blocks = parseContentBlocks(chapter.content);
|
const blocks = parseContentBlocks(chapter.content);
|
||||||
|
|
||||||
blocks.forEach((block, blockIdx) => {
|
blocks.forEach((block, blockIdx) => {
|
||||||
@@ -2346,7 +2361,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} else if (block.type === 'hr') {
|
} else if (block.type === 'hr') {
|
||||||
blockEl = document.createElement('hr');
|
blockEl = document.createElement('hr');
|
||||||
} else {
|
} else {
|
||||||
// paragraph
|
|
||||||
blockEl = document.createElement('p');
|
blockEl = document.createElement('p');
|
||||||
if (block.align) {
|
if (block.align) {
|
||||||
blockEl.style.textAlign = block.align;
|
blockEl.style.textAlign = block.align;
|
||||||
@@ -2357,14 +2371,242 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (blockEl) {
|
if (blockEl) {
|
||||||
blockEl.setAttribute('data-block-idx', domIdx.toString());
|
blockEl.setAttribute('data-block-idx', domIdx.toString());
|
||||||
readerContentBody.appendChild(blockEl);
|
blockEl.setAttribute('data-chapter-idx', chapterIdx.toString());
|
||||||
|
frag.appendChild(blockEl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function applyScroll() {
|
return frag;
|
||||||
if (!readerScrollArea) return;
|
}
|
||||||
|
|
||||||
|
function buildChapterFooterDOM(chapterIdx) {
|
||||||
|
if (!currentBook || !currentBook.chapters) return null;
|
||||||
|
const totalCh = currentBook.chapters.length;
|
||||||
|
const isLastChapter = (chapterIdx >= totalCh - 1);
|
||||||
|
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.className = 'reader-chapter-footer';
|
||||||
|
footer.id = `reader-chapter-footer-${chapterIdx}`;
|
||||||
|
footer.setAttribute('data-footer-for-ch', chapterIdx.toString());
|
||||||
|
|
||||||
|
if (isLastChapter) {
|
||||||
|
footer.classList.add('reader-book-finished');
|
||||||
|
footer.innerHTML = `
|
||||||
|
<div class="chapter-footer-status">
|
||||||
|
<span class="status-check">🎉</span>
|
||||||
|
<strong style="color: var(--primary); font-size: 16px;">Книга прочитана!</strong>
|
||||||
|
</div>
|
||||||
|
<p class="chapter-footer-hint">Поздравляем! Вы завершили чтение «${escapeHtml(currentBook.title)}».</p>
|
||||||
|
<div class="chapter-footer-nav">
|
||||||
|
<button type="button" class="btn btn-trainer btn-sm reader-footer-nav-btn" id="footer-train-btn-${chapterIdx}">
|
||||||
|
🎓 Тренировать слова книги
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline btn-sm reader-footer-nav-btn" id="footer-shelf-btn-${chapterIdx}">
|
||||||
|
📚 К библиотеке
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const trainBtn = footer.querySelector(`#footer-train-btn-${chapterIdx}`);
|
||||||
|
if (trainBtn) {
|
||||||
|
trainBtn.onclick = () => {
|
||||||
|
const tag = slugify(currentBook.title);
|
||||||
|
window.location.hash = `#srs/${encodeURIComponent(tag)}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const shelfBtn = footer.querySelector(`#footer-shelf-btn-${chapterIdx}`);
|
||||||
|
if (shelfBtn) {
|
||||||
|
shelfBtn.onclick = () => {
|
||||||
|
window.location.hash = '#reader';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCh = chapterIdx + 1;
|
||||||
|
const nextTitle = currentBook.chapters[nextCh]?.title || `Глава ${nextCh + 1}`;
|
||||||
|
|
||||||
|
footer.innerHTML = `
|
||||||
|
<div class="chapter-footer-status">
|
||||||
|
<span class="status-check">✔</span>
|
||||||
|
<span>Глава ${chapterIdx + 1} завершена</span>
|
||||||
|
</div>
|
||||||
|
<div class="chapter-footer-nav">
|
||||||
|
${chapterIdx > 0 ? `
|
||||||
|
<button type="button" class="btn btn-outline btn-sm reader-footer-nav-btn prev" id="footer-prev-btn-${chapterIdx}">
|
||||||
|
◀ Назад
|
||||||
|
</button>` : ''}
|
||||||
|
<button type="button" class="btn btn-primary btn-sm reader-footer-nav-btn next" id="footer-next-btn-${chapterIdx}">
|
||||||
|
Следующая: ${escapeHtml(nextTitle)} ▶
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="chapter-footer-hint">
|
||||||
|
${readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext
|
||||||
|
? '⚡ Автоподгрузка включена: следующая глава подгрузится автоматически при скролле вниз'
|
||||||
|
: '💡 Нажмите кнопку выше для перехода к следующей главе'}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nextBtn = footer.querySelector(`#footer-next-btn-${chapterIdx}`);
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.onclick = () => {
|
||||||
|
if (readerSettings.flowMode === 'continuous') {
|
||||||
|
const target = document.getElementById(`chapter-anchor-${nextCh}`);
|
||||||
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else if (readerSettings.autoLoadNext) {
|
||||||
|
appendNextChapter(nextCh, true);
|
||||||
|
} else {
|
||||||
|
window.location.hash = `#reader/book/${currentBook.id}/ch/${nextCh}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevBtn = footer.querySelector(`#footer-prev-btn-${chapterIdx}`);
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.onclick = () => {
|
||||||
|
window.location.hash = `#reader/book/${currentBook.id}/ch/${chapterIdx - 1}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendNextChapter(nextCh, smoothScroll = false) {
|
||||||
|
if (!currentBook || !currentBook.chapters || nextCh >= currentBook.chapters.length) return;
|
||||||
|
if (loadedChaptersSet.has(nextCh)) {
|
||||||
|
if (smoothScroll) {
|
||||||
|
const target = document.getElementById(`chapter-anchor-${nextCh}`);
|
||||||
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldFooter = readerContentBody.querySelector(`.reader-chapter-footer[data-footer-for-ch="${nextCh - 1}"]`);
|
||||||
|
if (oldFooter) {
|
||||||
|
oldFooter.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownWordsMap = new Map();
|
||||||
|
allCards.forEach(c => {
|
||||||
|
if (c.term) knownWordsMap.set(c.term.trim().toLowerCase(), c);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chFrag = buildChapterDOM(nextCh, knownWordsMap, true);
|
||||||
|
readerContentBody.appendChild(chFrag);
|
||||||
|
|
||||||
|
const newFooter = buildChapterFooterDOM(nextCh);
|
||||||
|
if (newFooter) {
|
||||||
|
readerContentBody.appendChild(newFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedChaptersSet.add(nextCh);
|
||||||
|
|
||||||
|
readerPrevChBtn.disabled = false;
|
||||||
|
readerNextChBtn.disabled = nextCh >= currentBook.chapters.length - 1;
|
||||||
|
|
||||||
|
if (smoothScroll) {
|
||||||
|
const anchor = document.getElementById(`chapter-anchor-${nextCh}`);
|
||||||
|
if (anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContinuousBook(targetChapter = 0, restoreScrollTop = null, restoreFraction = null, restoreAnchorIdx = null) {
|
||||||
|
if (!currentBook || !currentBook.chapters) return;
|
||||||
|
|
||||||
|
currentChapterIndex = Math.max(0, Math.min(targetChapter, currentBook.chapters.length - 1));
|
||||||
|
readerChapterSelect.value = currentChapterIndex;
|
||||||
|
|
||||||
|
readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
||||||
|
readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
||||||
|
|
||||||
|
readerContentBody.innerHTML = '';
|
||||||
|
loadedChaptersSet.clear();
|
||||||
|
|
||||||
|
const knownWordsMap = new Map();
|
||||||
|
allCards.forEach(c => {
|
||||||
|
if (c.term) knownWordsMap.set(c.term.trim().toLowerCase(), c);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentBook.chapters.forEach((_, idx) => {
|
||||||
|
loadedChaptersSet.add(idx);
|
||||||
|
const chFrag = buildChapterDOM(idx, knownWordsMap, idx > 0);
|
||||||
|
readerContentBody.appendChild(chFrag);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalFooter = buildChapterFooterDOM(currentBook.chapters.length - 1);
|
||||||
|
if (finalFooter) {
|
||||||
|
readerContentBody.appendChild(finalFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScroll(restoreAnchorIdx, restoreScrollTop, restoreFraction, currentChapterIndex);
|
||||||
|
|
||||||
|
const posToSave = (restoreFraction !== null && restoreFraction > 0) ? restoreFraction : 0;
|
||||||
|
fetch(`/api/books/${currentBook.id}/progress`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ current_chapter: currentChapterIndex, current_position: posToSave }),
|
||||||
|
}).catch(err => console.log('Progress save error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChapter(index, restoreScrollTop = null, restoreFraction = null, restoreAnchorIdx = null) {
|
||||||
|
if (!currentBook || !currentBook.chapters) return;
|
||||||
|
|
||||||
|
if (readerSettings.flowMode === 'continuous') {
|
||||||
|
renderContinuousBook(index, restoreScrollTop, restoreFraction, restoreAnchorIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChapterIndex = Math.max(0, Math.min(index, currentBook.chapters.length - 1));
|
||||||
|
readerChapterSelect.value = currentChapterIndex;
|
||||||
|
|
||||||
|
// Refresh status icons in chapter select
|
||||||
|
if (readerChapterSelect.options) {
|
||||||
|
for (let i = 0; i < readerChapterSelect.options.length; i++) {
|
||||||
|
const rawTitle = currentBook.chapters[i]?.title || `Глава ${i + 1}`;
|
||||||
|
const icon = i < currentChapterIndex ? '✔ ' : (i === currentChapterIndex ? '▶ ' : '○ ');
|
||||||
|
readerChapterSelect.options[i].textContent = `${icon}${rawTitle}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
||||||
|
readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
||||||
|
|
||||||
|
readerContentBody.innerHTML = '';
|
||||||
|
loadedChaptersSet.clear();
|
||||||
|
loadedChaptersSet.add(currentChapterIndex);
|
||||||
|
|
||||||
|
const knownWordsMap = new Map();
|
||||||
|
allCards.forEach(c => {
|
||||||
|
if (c.term) knownWordsMap.set(c.term.trim().toLowerCase(), c);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chFrag = buildChapterDOM(currentChapterIndex, knownWordsMap, false);
|
||||||
|
readerContentBody.appendChild(chFrag);
|
||||||
|
|
||||||
|
const footer = buildChapterFooterDOM(currentChapterIndex);
|
||||||
|
if (footer) {
|
||||||
|
readerContentBody.appendChild(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScroll(restoreAnchorIdx, restoreScrollTop, restoreFraction, currentChapterIndex);
|
||||||
|
|
||||||
|
// Save progress to server
|
||||||
|
const posToSave = (restoreFraction !== null && restoreFraction > 0) ? restoreFraction : 0;
|
||||||
|
fetch(`/api/books/${currentBook.id}/progress`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ current_chapter: currentChapterIndex, current_position: posToSave }),
|
||||||
|
}).catch(err => console.log('Progress save error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyScroll(restoreAnchorIdx = null, restoreScrollTop = null, restoreFraction = null, chIdx = 0) {
|
||||||
|
if (!readerScrollArea) return;
|
||||||
|
|
||||||
|
function doScroll() {
|
||||||
if (restoreAnchorIdx !== null && restoreAnchorIdx !== undefined) {
|
if (restoreAnchorIdx !== null && restoreAnchorIdx !== undefined) {
|
||||||
const el = readerContentBody.querySelector(`[data-block-idx="${restoreAnchorIdx}"]`);
|
const el = readerContentBody.querySelector(`[data-chapter-idx="${chIdx}"][data-block-idx="${restoreAnchorIdx}"]`)
|
||||||
|
|| readerContentBody.querySelector(`[data-block-idx="${restoreAnchorIdx}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
const containerTop = readerScrollArea.getBoundingClientRect().top;
|
const containerTop = readerScrollArea.getBoundingClientRect().top;
|
||||||
const elTop = el.getBoundingClientRect().top;
|
const elTop = el.getBoundingClientRect().top;
|
||||||
@@ -2378,6 +2620,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} else if (restoreFraction !== null && restoreFraction > 0) {
|
} else if (restoreFraction !== null && restoreFraction > 0) {
|
||||||
const maxScroll = Math.max(0, readerScrollArea.scrollHeight - readerScrollArea.clientHeight);
|
const maxScroll = Math.max(0, readerScrollArea.scrollHeight - readerScrollArea.clientHeight);
|
||||||
readerScrollArea.scrollTop = Math.round(maxScroll * restoreFraction);
|
readerScrollArea.scrollTop = Math.round(maxScroll * restoreFraction);
|
||||||
|
} else if (chIdx > 0) {
|
||||||
|
const anchor = document.getElementById(`chapter-anchor-${chIdx}`);
|
||||||
|
if (anchor) {
|
||||||
|
const containerTop = readerScrollArea.getBoundingClientRect().top;
|
||||||
|
const elTop = anchor.getBoundingClientRect().top;
|
||||||
|
readerScrollArea.scrollTop += (elTop - containerTop - 10);
|
||||||
|
} else {
|
||||||
|
readerScrollArea.scrollTop = 0;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
readerScrollArea.scrollTop = 0;
|
readerScrollArea.scrollTop = 0;
|
||||||
}
|
}
|
||||||
@@ -2385,17 +2636,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
applyScroll();
|
doScroll();
|
||||||
setTimeout(applyScroll, 100);
|
setTimeout(doScroll, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save progress to server
|
|
||||||
const posToSave = (restoreFraction !== null && restoreFraction > 0) ? restoreFraction : 0;
|
|
||||||
fetch(`/api/books/${currentBook.id}/progress`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ current_chapter: currentChapterIndex, current_position: posToSave }),
|
|
||||||
}).catch(err => console.log('Progress save error:', err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse text into structured semantic blocks
|
// Parse text into structured semantic blocks
|
||||||
@@ -3054,20 +3297,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapter Navigation & Reader Toolbar (URL Hash Driven)
|
// Chapter Navigation & Reader Toolbar (URL Hash & Continuous Scroll Driven)
|
||||||
if (readerChapterSelect) {
|
if (readerChapterSelect) {
|
||||||
readerChapterSelect.addEventListener('change', () => {
|
readerChapterSelect.addEventListener('change', () => {
|
||||||
const ch = parseInt(readerChapterSelect.value, 10);
|
const ch = parseInt(readerChapterSelect.value, 10);
|
||||||
if (currentBook) {
|
if (!currentBook) return;
|
||||||
window.location.hash = `#reader/book/${currentBook.id}/ch/${ch}`;
|
if (readerSettings.flowMode === 'continuous' || loadedChaptersSet.has(ch)) {
|
||||||
|
currentChapterIndex = ch;
|
||||||
|
const target = document.getElementById(`chapter-anchor-${ch}`)
|
||||||
|
|| readerContentBody.querySelector(`[data-chapter-idx="${ch}"]`);
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
window.location.hash = `#reader/book/${currentBook.id}/ch/${ch}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readerPrevChBtn) {
|
if (readerPrevChBtn) {
|
||||||
readerPrevChBtn.addEventListener('click', () => {
|
readerPrevChBtn.addEventListener('click', () => {
|
||||||
if (currentBook && currentChapterIndex > 0) {
|
if (currentBook && currentChapterIndex > 0) {
|
||||||
window.location.hash = `#reader/book/${currentBook.id}/ch/${currentChapterIndex - 1}`;
|
const prev = currentChapterIndex - 1;
|
||||||
|
if (readerSettings.flowMode === 'continuous' || loadedChaptersSet.has(prev)) {
|
||||||
|
currentChapterIndex = prev;
|
||||||
|
if (readerChapterSelect) readerChapterSelect.value = prev;
|
||||||
|
const target = document.getElementById(`chapter-anchor-${prev}`)
|
||||||
|
|| readerContentBody.querySelector(`[data-chapter-idx="${prev}"]`);
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.location.hash = `#reader/book/${currentBook.id}/ch/${prev}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3075,7 +3337,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (readerNextChBtn) {
|
if (readerNextChBtn) {
|
||||||
readerNextChBtn.addEventListener('click', () => {
|
readerNextChBtn.addEventListener('click', () => {
|
||||||
if (currentBook && currentBook.chapters && currentChapterIndex < currentBook.chapters.length - 1) {
|
if (currentBook && currentBook.chapters && currentChapterIndex < currentBook.chapters.length - 1) {
|
||||||
window.location.hash = `#reader/book/${currentBook.id}/ch/${currentChapterIndex + 1}`;
|
const next = currentChapterIndex + 1;
|
||||||
|
if (readerSettings.flowMode === 'continuous' || loadedChaptersSet.has(next)) {
|
||||||
|
currentChapterIndex = next;
|
||||||
|
if (readerChapterSelect) readerChapterSelect.value = next;
|
||||||
|
const target = document.getElementById(`chapter-anchor-${next}`)
|
||||||
|
|| readerContentBody.querySelector(`[data-chapter-idx="${next}"]`);
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext) {
|
||||||
|
appendNextChapter(next, true);
|
||||||
|
} else {
|
||||||
|
window.location.hash = `#reader/book/${currentBook.id}/ch/${next}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3112,8 +3389,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveVisibleChapter() {
|
||||||
|
if (!readerScrollArea || !readerContentBody) return currentChapterIndex;
|
||||||
|
const dividers = readerContentBody.querySelectorAll('[data-chapter-idx]');
|
||||||
|
if (!dividers || dividers.length === 0) return currentChapterIndex;
|
||||||
|
|
||||||
|
const containerTop = readerScrollArea.getBoundingClientRect().top;
|
||||||
|
let activeIdx = currentChapterIndex;
|
||||||
|
|
||||||
|
for (let el of dividers) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.top <= containerTop + 140) {
|
||||||
|
const ch = parseInt(el.getAttribute('data-chapter-idx'), 10);
|
||||||
|
if (!isNaN(ch)) activeIdx = ch;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeIdx;
|
||||||
|
}
|
||||||
|
|
||||||
function updateReaderProgressBar() {
|
function updateReaderProgressBar() {
|
||||||
const { pct, fraction } = getReaderScrollMetrics();
|
const { pct, fraction } = getReaderScrollMetrics();
|
||||||
|
|
||||||
|
if (readerSettings.flowMode === 'continuous') {
|
||||||
|
if (readerChapterProgressFill) {
|
||||||
|
readerChapterProgressFill.style.width = `${pct}%`;
|
||||||
|
}
|
||||||
|
if (readerScrollPctBadge) {
|
||||||
|
readerScrollPctBadge.textContent = `${pct}%`;
|
||||||
|
const totalCh = currentBook && currentBook.chapters ? currentBook.chapters.length : 1;
|
||||||
|
readerScrollPctBadge.title = `Вся книга: ${pct}% • Глава ${currentChapterIndex + 1} из ${totalCh}`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters mode
|
||||||
if (readerChapterProgressFill) {
|
if (readerChapterProgressFill) {
|
||||||
readerChapterProgressFill.style.width = `${pct}%`;
|
readerChapterProgressFill.style.width = `${pct}%`;
|
||||||
}
|
}
|
||||||
@@ -3136,10 +3447,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (!currentBook || !readerScrollArea) return;
|
if (!currentBook || !readerScrollArea) return;
|
||||||
updateReaderProgressBar();
|
updateReaderProgressBar();
|
||||||
|
|
||||||
|
// 1. Detect active visible chapter in continuous or multi-chapter view
|
||||||
|
if (readerSettings.flowMode === 'continuous' || loadedChaptersSet.size > 1) {
|
||||||
|
const activeCh = getActiveVisibleChapter();
|
||||||
|
if (activeCh !== null && activeCh !== undefined && activeCh !== currentChapterIndex) {
|
||||||
|
currentChapterIndex = activeCh;
|
||||||
|
if (readerChapterSelect) readerChapterSelect.value = currentChapterIndex;
|
||||||
|
if (readerChapterSelect && readerChapterSelect.options) {
|
||||||
|
for (let i = 0; i < readerChapterSelect.options.length; i++) {
|
||||||
|
const rawTitle = currentBook.chapters[i]?.title || `Глава ${i + 1}`;
|
||||||
|
const icon = i < currentChapterIndex ? '✔ ' : (i === currentChapterIndex ? '▶ ' : '○ ');
|
||||||
|
readerChapterSelect.options[i].textContent = `${icon}${rawTitle}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (readerPrevChBtn) readerPrevChBtn.disabled = currentChapterIndex <= 0;
|
||||||
|
if (readerNextChBtn) readerNextChBtn.disabled = currentChapterIndex >= currentBook.chapters.length - 1;
|
||||||
|
|
||||||
|
const desiredHash = `#reader/book/${currentBook.id}/ch/${currentChapterIndex}`;
|
||||||
|
if (window.location.hash !== desiredHash) {
|
||||||
|
history.replaceState(null, '', desiredHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Infinite scroll: auto-append next chapter in chapters mode
|
||||||
|
if (readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext && !isAppendingChapter) {
|
||||||
|
const distanceToBottom = readerScrollArea.scrollHeight - (readerScrollArea.scrollTop + readerScrollArea.clientHeight);
|
||||||
|
if (distanceToBottom < 450) {
|
||||||
|
const loadedArr = Array.from(loadedChaptersSet);
|
||||||
|
const highestCh = loadedArr.length > 0 ? Math.max(...loadedArr) : currentChapterIndex;
|
||||||
|
if (currentBook && currentBook.chapters && highestCh < currentBook.chapters.length - 1) {
|
||||||
|
isAppendingChapter = true;
|
||||||
|
appendNextChapter(highestCh + 1, false);
|
||||||
|
setTimeout(() => { isAppendingChapter = false; }, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { scrollTop, fraction } = getReaderScrollMetrics();
|
const { scrollTop, fraction } = getReaderScrollMetrics();
|
||||||
const anchorIdx = getActiveAnchorIndex();
|
const anchorIdx = getActiveAnchorIndex();
|
||||||
|
|
||||||
// 1. Instant local persistence
|
// 3. Instant local persistence
|
||||||
try {
|
try {
|
||||||
const key = `notes_book_${currentBook.id}_pos`;
|
const key = `notes_book_${currentBook.id}_pos`;
|
||||||
localStorage.setItem(key, JSON.stringify({
|
localStorage.setItem(key, JSON.stringify({
|
||||||
@@ -3153,7 +3501,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
localStorage.setItem('notes_active_view', 'reader-view');
|
localStorage.setItem('notes_active_view', 'reader-view');
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// 2. Debounced server sync (every 800ms)
|
// 4. Debounced server sync (every 800ms)
|
||||||
clearTimeout(readerScrollSaveTimer);
|
clearTimeout(readerScrollSaveTimer);
|
||||||
readerScrollSaveTimer = setTimeout(() => {
|
readerScrollSaveTimer = setTimeout(() => {
|
||||||
if (!currentBook) return;
|
if (!currentBook) return;
|
||||||
@@ -3281,6 +3629,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
btn.classList.toggle('active', btn.dataset.align === readerSettings.textAlign);
|
btn.classList.toggle('active', btn.dataset.align === readerSettings.textAlign);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (readerModeGroup) {
|
||||||
|
readerModeGroup.querySelectorAll('.seg-btn').forEach(btn => {
|
||||||
|
btn.classList.toggle('active', btn.dataset.flow === readerSettings.flowMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingAutoloadRow) {
|
||||||
|
settingAutoloadRow.style.display = (readerSettings.flowMode === 'chapters') ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readerAutoloadGroup) {
|
||||||
|
readerAutoloadGroup.querySelectorAll('.seg-btn').forEach(btn => {
|
||||||
|
btn.classList.toggle('active', (btn.dataset.autoload === 'true') === readerSettings.autoLoadNext);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionSettingsPanel() {
|
function positionSettingsPanel() {
|
||||||
@@ -3406,6 +3770,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (readerModeGroup) {
|
||||||
|
readerModeGroup.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.seg-btn');
|
||||||
|
if (btn && btn.dataset.flow) {
|
||||||
|
if (readerSettings.flowMode !== btn.dataset.flow) {
|
||||||
|
readerSettings.flowMode = btn.dataset.flow;
|
||||||
|
saveReaderSettings();
|
||||||
|
updateReaderSettingsUI();
|
||||||
|
if (currentBook) {
|
||||||
|
renderChapter(currentChapterIndex, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readerAutoloadGroup) {
|
||||||
|
readerAutoloadGroup.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.seg-btn');
|
||||||
|
if (btn && btn.dataset.autoload !== undefined) {
|
||||||
|
readerSettings.autoLoadNext = (btn.dataset.autoload === 'true');
|
||||||
|
saveReaderSettings();
|
||||||
|
updateReaderSettingsUI();
|
||||||
|
const footerHint = readerContentBody.querySelector('.chapter-footer-hint');
|
||||||
|
if (footerHint) {
|
||||||
|
footerHint.textContent = (readerSettings.flowMode === 'chapters' && readerSettings.autoLoadNext)
|
||||||
|
? '⚡ Автоподгрузка включена: следующая глава подгрузится автоматически при скролле вниз'
|
||||||
|
: '💡 Нажмите кнопку выше для перехода к следующей главе';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (readerSettingsPanel && !readerSettingsPanel.classList.contains('hidden')) {
|
if (readerSettingsPanel && !readerSettingsPanel.classList.contains('hidden')) {
|
||||||
if (!readerSettingsPanel.contains(e.target) && e.target !== readerSettingsBtn && !readerSettingsBtn.contains(e.target)) {
|
if (!readerSettingsPanel.contains(e.target) && e.target !== readerSettingsBtn && !readerSettingsBtn.contains(e.target)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user