1321 lines
67 KiB
HTML
1321 lines
67 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>Алетея: чтение</title>
|
|
<style>
|
|
* { /* Сброс отступов и установка box-sizing для всех элементов */
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--reader-safe-left: 0px;
|
|
--reader-safe-top: 0px;
|
|
--reader-safe-right: 0px;
|
|
--reader-safe-bottom: 0px;
|
|
}
|
|
|
|
html, body { /* Растягиваем страницу на весь экран, отключаем прокрутку и выделение текста */
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background-color: #faf8ef; /* Цвет "сепия" для комфортного чтения */
|
|
font-family: serif;
|
|
text-rendering: optimizeLegibility;
|
|
-webkit-font-smoothing: antialiased;
|
|
-webkit-touch-callout: default;
|
|
-webkit-user-select: text;
|
|
user-select: text;
|
|
}
|
|
|
|
#reader-container { /* Основной контейнер для книги */
|
|
position: fixed;
|
|
top: var(--reader-safe-top);
|
|
right: var(--reader-safe-right);
|
|
bottom: var(--reader-safe-bottom);
|
|
left: var(--reader-safe-left);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#book-content, #fb2-content { /* Контейнеры для EPUB и FB2 соответственно */
|
|
width: 100%;
|
|
height: 100%;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
#fb2-content { /* Специфичные настройки для FB2: отступы и скрытие по умолчанию */
|
|
overflow: hidden;
|
|
padding: 28px 24px;
|
|
font-size: 18px;
|
|
line-height: 1.68;
|
|
display: none;
|
|
hyphens: auto;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
#loading { /* Центрированный экран загрузки */
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100%;
|
|
padding: 32px;
|
|
text-align: center;
|
|
background:
|
|
radial-gradient(circle at 50% 42%, rgba(63, 111, 90, 0.12), transparent 32%),
|
|
linear-gradient(180deg, #FAF1E2 0%, #FFFDF8 100%);
|
|
}
|
|
|
|
.loading-panel {
|
|
width: min(320px, 100%);
|
|
padding: 28px 24px 26px;
|
|
border: 1px solid rgba(215, 205, 191, 0.86);
|
|
border-radius: 28px;
|
|
background: rgba(255, 252, 246, 0.88);
|
|
box-shadow: 0 18px 44px rgba(47, 38, 28, 0.10);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.loading-mark {
|
|
width: 58px;
|
|
height: 58px;
|
|
border-radius: 20px;
|
|
background: #ECE5DB;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
box-shadow: inset 0 0 0 1px rgba(215, 205, 191, 0.92);
|
|
}
|
|
|
|
.loading-title {
|
|
color: #24201B;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
#loading-text {
|
|
color: #6B6258;
|
|
font-size: 14px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.spinner { /* Анимация крутящегося индикатора загрузки */
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid rgba(63, 111, 90, 0.18);
|
|
border-top-color: #3F6F5A;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin { /* Описание вращения спиннера */
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
#error-display { /* Контейнер для вывода ошибок */
|
|
display: none;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100%;
|
|
font-size: 16px;
|
|
color: #B4493B;
|
|
padding: 32px;
|
|
text-align: center;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.book-search-highlight {
|
|
background: rgba(226, 169, 66, 0.45);
|
|
border-radius: 3px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="reader-container">
|
|
<div id="book-content"></div> <div id="fb2-content"></div> <div id="loading">
|
|
<div class="loading-panel">
|
|
<div class="loading-mark">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<span class="loading-title">Открываю книгу</span>
|
|
<span id="loading-text">Подготавливаю читалку...</span>
|
|
</div>
|
|
</div>
|
|
<div id="error-display">
|
|
<span style="font-size:40px">!</span>
|
|
<span id="error-text"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="_framework/hybridwebview.js"></script>
|
|
<script src="js/jszip.min.js"></script>
|
|
<script src="js/epub.min.js"></script>
|
|
<script>
|
|
(function () { // Самовызывающаяся функция для изоляции переменных
|
|
'use strict';
|
|
|
|
// ========== РљРРЁРРОВАННЫЕ DOM-РЛЕМЕНТЫ ==========
|
|
const $ = id => document.getElementById(id); // Короткий псевдоним для поиска элементов
|
|
const els = { // Объект со ссылками на элементы для быстрого доступа
|
|
loading: $('loading'),
|
|
loadingText: $('loading-text'),
|
|
errorDisplay: $('error-display'),
|
|
errorText: $('error-text'),
|
|
bookContent: $('book-content'),
|
|
fb2Content: $('fb2-content'),
|
|
};
|
|
|
|
// ========== РЎРћРЎРўРћРЇРќРР• (STATE) ==========
|
|
const state = { // Глобальный объект состояния текущей книги
|
|
book: null, // Объект книги epub.js
|
|
rendition: null, // Объект отображения epub.js
|
|
currentCfi: null, // Текущая позиция (идентификатор) в EPUB
|
|
totalPages: 0, // Всего страниц
|
|
bookFormat: '', // epub или fb2
|
|
isBookLoaded: false,
|
|
fb2CurrentPage: 0,
|
|
fb2TotalPages: 1,
|
|
toc: [], // Оглавление
|
|
lastCfi: null,
|
|
currentPage: 0,
|
|
currentFontSize: 18,
|
|
currentFontFamily: 'serif',
|
|
currentTheme: 'sepia',
|
|
brightness: 100,
|
|
safeInsets: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
relayoutTimer: null,
|
|
relayoutGeneration: 0,
|
|
resizeObserver: null,
|
|
search: {
|
|
query: '',
|
|
results: [],
|
|
currentIndex: -1,
|
|
generation: 0,
|
|
searching: false
|
|
},
|
|
lastSelection: null
|
|
};
|
|
|
|
// ========== РЈРўРР›РРўР« ==========
|
|
function debugLog(msg) { // Логирование в консоль с префиксом
|
|
console.log('[Reader] ' + msg);
|
|
}
|
|
|
|
function showError(msg) { // Показ экрана ошибки пользователю
|
|
els.loading.style.display = 'none';
|
|
els.errorDisplay.style.display = 'flex';
|
|
els.errorText.textContent = msg;
|
|
debugLog('ERROR: ' + msg);
|
|
}
|
|
|
|
function setLoadingText(msg) { // Обновление текста на экране загрузки
|
|
if (els.loadingText) els.loadingText.textContent = msg;
|
|
debugLog(msg);
|
|
}
|
|
|
|
const _escDiv = document.createElement('div'); // Буфер для очистки HTML
|
|
function escapeHtml(text) { // Защита от XSS: превращает < в < и т.д.
|
|
_escDiv.textContent = text;
|
|
return _escDiv.innerHTML;
|
|
}
|
|
|
|
function base64ToArrayBuffer(base64) { // Конвертация данных из строки (Base64) в бинарный массив
|
|
const bin = atob(base64); // Декодирование base64
|
|
const len = bin.length;
|
|
const buf = new ArrayBuffer(len);
|
|
const view = new Uint8Array(buf);
|
|
for (let i = 0; i < len; i++) {
|
|
view[i] = bin.charCodeAt(i); // Заполнение массива байтами
|
|
}
|
|
return buf;
|
|
}
|
|
function calculateOptimalLocationSize() {
|
|
// 1. Получаем размеры видимой области
|
|
const readerBounds = $('reader-container');
|
|
const width = readerBounds && readerBounds.clientWidth ? readerBounds.clientWidth : window.innerWidth;
|
|
const height = readerBounds && readerBounds.clientHeight ? readerBounds.clientHeight : window.innerHeight;
|
|
|
|
// 2. Получаем текущий размер шрифта (из состояния или напрямую из настроек)
|
|
// Если размер шрифта еще не задан, используем 18px по умолчанию
|
|
const fontSize = state.currentFontSize || 18;
|
|
|
|
// 3. Рмпирический коэффициент:
|
|
// На 1 квадратный пиксель при 18-м шрифте приходится примерно 0.005 - 0.007 символа.
|
|
// Мы рассчитываем "площадь" одного символа.
|
|
// Чем больше шрифт, тем больше места занимает символ (квадратичная зависимость).
|
|
const charArea = (fontSize * fontSize) * 0.55;
|
|
|
|
// 4. Рассчитываем общую вместимость экрана в символах
|
|
const screenArea = width * height;
|
|
let charactersPerScreen = Math.floor(screenArea / charArea);
|
|
|
|
// 5. Ограничиваем значения для стабильности epub.js
|
|
// Минимум 400 (чтобы не плодить тысячи локаций на маленьких текстах)
|
|
// Максимум 1500 (чтобы избежать "залипания" на больших экранах)
|
|
const finalSize = Math.max(400, Math.min(1000, charactersPerScreen));
|
|
|
|
debugLog(`Расчет локации: Ркран ${width}x${height}, Шрифт ${fontSize}px => Размер локации: ${finalSize}`);
|
|
|
|
return finalSize;
|
|
}
|
|
// ========== MESSAGE BRIDGE (Связь с приложением) ==========
|
|
function sendMessage(action, data) { // Отправка данных в нативный код (C# / Swift / Kotlin)
|
|
const message = JSON.stringify({ action, data: data || {} });
|
|
try {
|
|
if (window.HybridWebView && typeof window.HybridWebView.SendRawMessage === 'function') {
|
|
window.HybridWebView.SendRawMessage(message);
|
|
} else if (window.AndroidBridge && typeof window.AndroidBridge.postMessage === 'function') {
|
|
window.AndroidBridge.postMessage(message);
|
|
}
|
|
} catch (e) {
|
|
debugLog('Bridge error: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function applySafeAreaInsets() {
|
|
const insets = state.safeInsets;
|
|
document.documentElement.style.setProperty('--reader-safe-left', insets.left + 'px');
|
|
document.documentElement.style.setProperty('--reader-safe-top', insets.top + 'px');
|
|
document.documentElement.style.setProperty('--reader-safe-right', insets.right + 'px');
|
|
document.documentElement.style.setProperty('--reader-safe-bottom', insets.bottom + 'px');
|
|
|
|
scheduleReaderRelayout('safe-area', true);
|
|
}
|
|
|
|
window.setSafeAreaInsets = function (left, top, right, bottom) {
|
|
const nextInsets = {
|
|
left: Math.max(0, Number(left) || 0),
|
|
top: Math.max(0, Number(top) || 0),
|
|
right: Math.max(0, Number(right) || 0),
|
|
bottom: Math.max(0, Number(bottom) || 0)
|
|
};
|
|
|
|
if (
|
|
state.safeInsets.left === nextInsets.left &&
|
|
state.safeInsets.top === nextInsets.top &&
|
|
state.safeInsets.right === nextInsets.right &&
|
|
state.safeInsets.bottom === nextInsets.bottom
|
|
) {
|
|
return;
|
|
}
|
|
|
|
state.safeInsets = nextInsets;
|
|
applySafeAreaInsets();
|
|
};
|
|
|
|
function currentEpubCfi() {
|
|
return state.currentCfi || state.lastCfi || null;
|
|
}
|
|
|
|
function currentFb2Progress() {
|
|
const total = state.fb2TotalPages || 1;
|
|
return total > 1 ? state.fb2CurrentPage / (total - 1) : 0;
|
|
}
|
|
|
|
function relayoutEpubReader(preservePosition) {
|
|
if (!state.rendition || typeof state.rendition.resize !== 'function') {
|
|
return;
|
|
}
|
|
|
|
const cfiToKeep = preservePosition ? currentEpubCfi() : null;
|
|
const width = els.bookContent.clientWidth;
|
|
const height = els.bookContent.clientHeight;
|
|
|
|
if (width > 0 && height > 0) {
|
|
state.rendition.resize(width, height);
|
|
} else {
|
|
state.rendition.resize();
|
|
}
|
|
|
|
if (!cfiToKeep) {
|
|
return;
|
|
}
|
|
|
|
const generation = ++state.relayoutGeneration;
|
|
setTimeout(() => {
|
|
if (generation !== state.relayoutGeneration || !state.rendition) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = state.rendition.display(cfiToKeep);
|
|
if (result && typeof result.catch === 'function') {
|
|
result.catch(error => debugLog('EPUB relayout display failed: ' + error.message));
|
|
}
|
|
} catch (e) {
|
|
debugLog('EPUB relayout failed: ' + e.message);
|
|
}
|
|
}, 60);
|
|
}
|
|
|
|
function relayoutReader(reason, preservePosition) {
|
|
debugLog('Relayout reader: ' + reason);
|
|
if (state.bookFormat === 'fb2') {
|
|
setupFb2Pagination(preservePosition);
|
|
} else if (state.bookFormat === 'epub') {
|
|
relayoutEpubReader(preservePosition);
|
|
}
|
|
}
|
|
|
|
function scheduleReaderRelayout(reason, preservePosition) {
|
|
clearTimeout(state.relayoutTimer);
|
|
state.relayoutTimer = setTimeout(() => {
|
|
state.relayoutTimer = null;
|
|
relayoutReader(reason || 'unknown', preservePosition !== false);
|
|
}, 120);
|
|
}
|
|
|
|
function initReaderResizeHandling() {
|
|
const container = $('reader-container');
|
|
if (window.ResizeObserver && container) {
|
|
state.resizeObserver = new ResizeObserver(() => scheduleReaderRelayout('container-resize', true));
|
|
state.resizeObserver.observe(container);
|
|
}
|
|
|
|
window.addEventListener('resize', () => scheduleReaderRelayout('window-resize', true));
|
|
window.addEventListener('orientationchange', () => scheduleReaderRelayout('orientation-change', true));
|
|
if (window.visualViewport) {
|
|
window.visualViewport.addEventListener('resize', () => scheduleReaderRelayout('viewport-resize', true));
|
|
}
|
|
}
|
|
|
|
// ========== TOUCH / SWIPE ==========
|
|
const gestureTargets = new WeakSet();
|
|
|
|
function getSelectionText(selectionProvider) {
|
|
try {
|
|
return selectionProvider ? (selectionProvider().toString() || '') : '';
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function shouldIgnoreGesture(event, selectionProvider) {
|
|
const target = event.target;
|
|
if (
|
|
target &&
|
|
target.closest &&
|
|
target.closest('a,button,input,textarea,select,[contenteditable="true"]')
|
|
) {
|
|
return true;
|
|
}
|
|
return getSelectionText(selectionProvider).trim().length > 0;
|
|
}
|
|
|
|
function performTapAction(clientX) {
|
|
const left = state.safeInsets.left || 0;
|
|
const right = window.innerWidth - (state.safeInsets.right || 0);
|
|
const width = Math.max(1, right - left);
|
|
const localX = clientX - left;
|
|
|
|
if (localX < width * 0.3) {
|
|
window.prevPage();
|
|
} else if (localX > width * 0.7) {
|
|
window.nextPage();
|
|
} else {
|
|
sendMessage('toggleMenu', {});
|
|
}
|
|
}
|
|
|
|
function attachReaderGestures(target, selectionProvider) {
|
|
if (!target || gestureTargets.has(target)) return;
|
|
gestureTargets.add(target);
|
|
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let startTime = 0;
|
|
let lastTouchActionAt = 0;
|
|
|
|
target.addEventListener('touchstart', event => {
|
|
if (event.touches.length !== 1) return;
|
|
const touch = event.touches[0];
|
|
startX = touch.clientX;
|
|
startY = touch.clientY;
|
|
startTime = Date.now();
|
|
}, { passive: true });
|
|
|
|
target.addEventListener('touchend', event => {
|
|
if (shouldIgnoreGesture(event, selectionProvider)) return;
|
|
const touch = event.changedTouches[0];
|
|
if (!touch) return;
|
|
|
|
const dx = touch.clientX - startX;
|
|
const dy = touch.clientY - startY;
|
|
const dt = Date.now() - startTime;
|
|
const horizontal = Math.abs(dx) > Math.abs(dy);
|
|
|
|
if (dt < 500 && horizontal && Math.abs(dx) > 50) {
|
|
dx < 0 ? window.nextPage() : window.prevPage();
|
|
lastTouchActionAt = Date.now();
|
|
} else if (dt < 280 && Math.abs(dx) < 12 && Math.abs(dy) < 12) {
|
|
performTapAction(touch.clientX);
|
|
lastTouchActionAt = Date.now();
|
|
}
|
|
}, { passive: true });
|
|
|
|
target.addEventListener('click', event => {
|
|
if (Date.now() - lastTouchActionAt < 450) return;
|
|
if (shouldIgnoreGesture(event, selectionProvider)) return;
|
|
performTapAction(event.clientX);
|
|
});
|
|
}
|
|
|
|
function initReaderGestures() {
|
|
attachReaderGestures(document, () => window.getSelection());
|
|
}
|
|
|
|
function attachEpubContentGestures(contents) {
|
|
if (!contents || !contents.document) return;
|
|
attachReaderGestures(contents.document, () => contents.window && contents.window.getSelection());
|
|
}
|
|
|
|
// ========== EPUB LOGIC ==========
|
|
function loadEpubFromBase64(base64Data, lastCfi, cachedLocations) {
|
|
setLoadingText('Подготавливаю EPUB...');
|
|
try {
|
|
const arrayBuffer = base64ToArrayBuffer(base64Data);
|
|
if (typeof JSZip === 'undefined') { showError('Не удалось открыть EPUB: модуль ZIP не загружен.'); return; }
|
|
if (typeof ePub === 'undefined') { showError('Не удалось открыть EPUB: модуль чтения не загружен.'); return; }
|
|
|
|
state.book = ePub(arrayBuffer); // Создание объекта книги из данных
|
|
state.lastCfi = null;
|
|
els.fb2Content.style.display = 'none';
|
|
els.bookContent.style.display = 'block';
|
|
|
|
// Рендеринг (отрисовка) книги в контейнер
|
|
state.rendition = state.book.renderTo('book-content', {
|
|
width: '100%',
|
|
height: '100%',
|
|
spread: 'none', // Без двухстраничного режима
|
|
flow: 'paginated' // Режим постраничного отображения
|
|
});
|
|
|
|
// Настройка стилей внутри фрейма книги
|
|
state.rendition.themes.default({
|
|
'body': {
|
|
'font-family': 'serif !important',
|
|
'font-size': '18px !important',
|
|
'line-height': '1.68 !important',
|
|
'padding': '28px 24px !important',
|
|
'background-color': '#faf8ef !important',
|
|
'color': '#333 !important',
|
|
'hyphens': 'auto !important',
|
|
'overflow-wrap': 'break-word !important',
|
|
'-webkit-user-select': 'text !important',
|
|
'user-select': 'text !important',
|
|
'-webkit-touch-callout': 'default !important'
|
|
},
|
|
'p': { 'text-indent': '1.35em', 'margin-bottom': '0.75em' }
|
|
});
|
|
|
|
state.book.ready.then(() => {
|
|
// Убираем экран загрузки СРАЗУ, как только книга готова к отрисовке первой страницы
|
|
els.loading.style.display = 'none';
|
|
|
|
// Обработка оглавления
|
|
const toc = state.book.navigation.toc || [];
|
|
const flattenToc = (items, depth) => (items || []).flatMap(ch => [
|
|
{ label: ch.label.trim(), href: ch.href, depth: depth || 0 },
|
|
...flattenToc(ch.subitems || [], (depth || 0) + 1)
|
|
]);
|
|
state.toc = flattenToc(toc, 0);
|
|
sendMessage('chaptersLoaded', { chapters: state.toc });
|
|
|
|
// ПРОВЕРКА РљРРЁРђ: Если РјС‹ СѓР¶Рµ передали сохраненные локации
|
|
if (cachedLocations) {
|
|
debugLog("Загрузка из кэша...");
|
|
state.book.locations.load(cachedLocations);
|
|
state.totalPages = state.book.locations.length();
|
|
state.lastCfi = null;
|
|
sendMessage('bookReady', { totalPages: state.totalPages });
|
|
} else {
|
|
// Рспользуем setTimeout, чтобы РЅРµ блокировать поток отрисовки
|
|
const dynamicSize = calculateOptimalLocationSize();
|
|
// Запускаем генерацию с динамическим размером
|
|
setTimeout(() => {
|
|
state.book.locations.generate(dynamicSize).then(() => {
|
|
state.totalPages = state.book.locations.length();
|
|
const locationsToSave = state.book.locations.save();
|
|
state.lastCfi = null;
|
|
|
|
// Отправляем в C#, чтобы сохранить на будущее
|
|
sendMessage('saveLocations', { locations: locationsToSave });
|
|
sendMessage('bookReady', { totalPages: state.totalPages });
|
|
});
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
// Событие при смене страницы
|
|
state.rendition.on('relocated', location => {
|
|
if (!location || !location.start) return;
|
|
const newCfi = location.start.cfi;
|
|
|
|
// Получаем процент прогресса
|
|
const progress = state.book.locations.percentageFromCfi(newCfi) || 0;
|
|
|
|
// Обновляем lastCfi
|
|
state.lastCfi = newCfi;
|
|
state.currentCfi = newCfi;
|
|
const generatedTotal = Math.max(1, state.book.locations.length() || state.totalPages || 1);
|
|
const locationIndex = state.book.locations.locationFromCfi(newCfi);
|
|
const generatedPage = Number.isFinite(locationIndex)
|
|
? locationIndex + 1
|
|
: Math.round(progress * Math.max(0, generatedTotal - 1)) + 1;
|
|
state.currentPage = Math.max(1, Math.min(generatedPage, generatedTotal));
|
|
state.totalPages = generatedTotal;
|
|
|
|
const chapterPage = location.start.displayed ? location.start.displayed.page : 1;
|
|
const chapterTotal = location.start.displayed ? location.start.displayed.total : 1;
|
|
const chapterName = chapterForHref(location.start.href || '');
|
|
|
|
// Отправка прогресса в приложение
|
|
// currentPage теперь показывает процент (округлённый до целого)
|
|
sendMessage('progressUpdate', {
|
|
progress: progress,
|
|
cfi: newCfi,
|
|
currentPage: state.currentPage,
|
|
totalPages: state.totalPages,
|
|
chapterCurrentPage: chapterPage,
|
|
chapterTotalPages: chapterTotal,
|
|
chapter: chapterName
|
|
});
|
|
});
|
|
|
|
state.rendition.on('selected', (cfiRange, contents) => {
|
|
let selectedText = '';
|
|
try {
|
|
selectedText = contents && contents.window
|
|
? contents.window.getSelection().toString()
|
|
: '';
|
|
} catch (e) {
|
|
selectedText = '';
|
|
}
|
|
state.lastSelection = {
|
|
cfi: cfiRange,
|
|
text: selectedText
|
|
};
|
|
});
|
|
|
|
state.rendition.on('rendered', (_section, view) => {
|
|
if (view && view.contents) {
|
|
attachEpubContentGestures(view.contents);
|
|
}
|
|
});
|
|
|
|
// Отображение книги на сохраненной позиции или в начале
|
|
const displayTarget = (lastCfi && lastCfi !== 'null' && lastCfi !== 'undefined') ? lastCfi : undefined;
|
|
state.rendition.display(displayTarget);
|
|
state.isBookLoaded = true;
|
|
|
|
} catch (e) { showError('EPUB load error: ' + e.message); }
|
|
}
|
|
|
|
// ========== FB2 LOGIC ==========
|
|
function loadFb2FromBase64(base64Data, lastPosition) {
|
|
setLoadingText('Подготавливаю FB2...');
|
|
try {
|
|
const arrayBuffer = base64ToArrayBuffer(base64Data);
|
|
const bytes = new Uint8Array(arrayBuffer);
|
|
let xmlText = new TextDecoder('utf-8').decode(bytes);
|
|
|
|
// Проверка кодировки в заголовке XML (если не UTF-8, перекодируем)
|
|
const encMatch = xmlText.match(/encoding=["\']([^"\']+)["\']/i);
|
|
if (encMatch && encMatch[1].toLowerCase() !== 'utf-8') {
|
|
xmlText = new TextDecoder(encMatch[1]).decode(bytes);
|
|
}
|
|
|
|
state.lastCfi = null;
|
|
els.bookContent.style.display = 'none';
|
|
els.fb2Content.style.display = 'block';
|
|
|
|
// Парсинг строки в XML-документ
|
|
const doc = new DOMParser().parseFromString(xmlText, 'text/xml');
|
|
if (doc.querySelector('parsererror')) { showError('Не удалось открыть FB2: ошибка разбора XML.'); return; }
|
|
|
|
const fb2Html = parseFb2Document(doc); // Преобразование FB2 XML в HTML
|
|
els.fb2Content.innerHTML = fb2Html.html;
|
|
els.loading.style.display = 'none';
|
|
|
|
// Рспользование анимационных кадров для замера размеров после вставки РІ DOM
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
setupFb2Pagination(false, () => { // Нарезка на колонки
|
|
state.isBookLoaded = true;
|
|
state.bookFormat = 'fb2';
|
|
if (lastPosition) goToFb2Position(parseFloat(lastPosition));
|
|
sendMessage('chaptersLoaded', { chapters: fb2Html.chapters });
|
|
sendMessage('bookReady', { totalPages: state.totalPages });
|
|
updateFb2Progress();
|
|
});
|
|
});
|
|
});
|
|
} catch (e) { showError('FB2 error: ' + e.message); }
|
|
}
|
|
|
|
// ========== FB2 PARSER (Преобразование тегов) ==========
|
|
function localTag(el) { // Утилита для получения имени тега без пространств имен (типа fb2:)
|
|
return el.tagName ? el.tagName.toLowerCase().replace(/.*:/, '') : '';
|
|
}
|
|
|
|
function parseFb2Document(doc) {
|
|
const chapters = [];
|
|
const parts = ['<div id="fb2-inner" style="font-size:18px;font-family:serif;line-height:1.68;-webkit-user-select:text;user-select:text;-webkit-touch-callout:default;">'];
|
|
let bodies = doc.querySelectorAll('body'); // FB2 может иметь несколько body (основной текст, сноски)
|
|
|
|
let chapterIndex = 0;
|
|
for (const body of bodies) {
|
|
for (const child of body.children) {
|
|
if (localTag(child) === 'section') { // Секции — это главы
|
|
const result = parseFb2Section(child, chapterIndex);
|
|
parts.push(result.html);
|
|
chapters.push(...result.chapters);
|
|
chapterIndex += Math.max(result.chapters.length, 1);
|
|
}
|
|
}
|
|
}
|
|
parts.push('</div>');
|
|
return { html: parts.join(''), chapters };
|
|
}
|
|
|
|
// Обработчики конкретных тегов FB2
|
|
const sectionTagHandlers = {
|
|
title(child, idx, chapters) { // Заголовки глав
|
|
const text = (child.textContent || '').trim();
|
|
chapters.push({ label: text, href: idx.toString() });
|
|
return `<h2 class="fb2-title" data-chapter="${idx}" style="text-align:center;margin:1em 0 .5em">${escapeHtml(text)}</h2>`;
|
|
},
|
|
p(child) { // Абзацы
|
|
return `<p style="text-indent:1.5em;margin-bottom:.3em">${getInlineHtml(child)}</p>`;
|
|
},
|
|
'empty-line'() { return '<br/>'; },
|
|
subtitle(child) { return `<h3 style="text-align:center;margin:.8em 0">${escapeHtml(child.textContent || '')}</h3>`; },
|
|
epigraph(child) { return `<blockquote style="font-style:italic;margin:1em 2em">${parseInnerParagraphs(child)}</blockquote>`; },
|
|
poem(child) { return `<div style="margin:1em 2em">${parsePoem(child)}</div>`; },
|
|
cite(child) { return `<blockquote style="margin:1em 2em;padding-left:1em;border-left:3px solid #ccc">${parseInnerParagraphs(child)}</blockquote>`; },
|
|
};
|
|
|
|
function parseFb2Section(section, startIndex) {
|
|
const chapters = [];
|
|
const parts = [`<div class="fb2-section" data-section="${startIndex}">`];
|
|
for (const child of section.children) {
|
|
const tag = localTag(child);
|
|
if (tag === 'section') { // Рекурсия для вложенных секций
|
|
const sub = parseFb2Section(child, startIndex + chapters.length);
|
|
parts.push(sub.html);
|
|
chapters.push(...sub.chapters);
|
|
} else if (sectionTagHandlers[tag]) {
|
|
parts.push(sectionTagHandlers[tag](child, startIndex + chapters.length, chapters));
|
|
}
|
|
}
|
|
parts.push('</div>');
|
|
return { html: parts.join(''), chapters };
|
|
}
|
|
|
|
function getInlineHtml(el) { // Обработка форматирования внутри строки (жирный, курсив)
|
|
const parts = [];
|
|
for (const node of el.childNodes) {
|
|
if (node.nodeType === 3) parts.push(escapeHtml(node.textContent)); // Текст
|
|
else if (node.nodeType === 1) { // Тег
|
|
const tag = localTag(node);
|
|
const inner = getInlineHtml(node);
|
|
if (tag === 'strong' || tag === 'bold') parts.push(`<strong>${inner}</strong>`);
|
|
else if (tag === 'emphasis' || tag === 'em') parts.push(`<em>${inner}</em>`);
|
|
else if (tag === 'strikethrough') parts.push(`<s>${inner}</s>`);
|
|
else parts.push(inner);
|
|
}
|
|
}
|
|
return parts.join('');
|
|
}
|
|
|
|
function parseInnerParagraphs(el) { // Парсинг группы абзацев (для цитат/эпиграфов)
|
|
const parts = [];
|
|
for (const child of el.children) {
|
|
const tag = localTag(child);
|
|
if (tag === 'p') parts.push(`<p>${getInlineHtml(child)}</p>`);
|
|
else if (tag === 'text-author') parts.push(`<p style="text-align:right;font-style:italic">— ${escapeHtml(child.textContent || '')}</p>`);
|
|
}
|
|
return parts.join('');
|
|
}
|
|
|
|
function parsePoem(el) { // Парсинг стихов (строфы и строки)
|
|
const parts = [];
|
|
for (const child of el.children) {
|
|
const tag = localTag(child);
|
|
if (tag === 'stanza') {
|
|
parts.push('<div style="margin-bottom:1em">');
|
|
for (const v of child.children) {
|
|
if (localTag(v) === 'v') parts.push(`<p style="text-indent:0">${escapeHtml(v.textContent || '')}</p>`);
|
|
}
|
|
parts.push('</div>');
|
|
} else if (tag === 'title') {
|
|
parts.push(`<h4>${escapeHtml(child.textContent || '')}</h4>`);
|
|
}
|
|
}
|
|
return parts.join('');
|
|
}
|
|
|
|
// ========== FB2 PAGINATION (Рмитация страниц через CSS Columns) ==========
|
|
function setupFb2Pagination(preservePosition, afterLayout) {
|
|
const container = els.fb2Content;
|
|
const inner = $('fb2-inner');
|
|
if (!container || !inner) return;
|
|
|
|
const w = container.clientWidth;
|
|
const h = container.clientHeight;
|
|
if (w <= 0 || h <= 0) return;
|
|
const progressToKeep = preservePosition ? currentFb2Progress() : 0;
|
|
|
|
// Основная магия: CSS превращает длинный текст в ряд колонок шириной с экран
|
|
Object.assign(inner.style, {
|
|
columnWidth: w + 'px',
|
|
columnGap: '0px',
|
|
columnFill: 'auto',
|
|
height: h + 'px',
|
|
overflow: 'hidden',
|
|
});
|
|
|
|
requestAnimationFrame(() => {
|
|
// Общая ширина контента делить на ширину экрана = количество страниц
|
|
state.fb2TotalPages = Math.max(1, Math.ceil(inner.scrollWidth / w));
|
|
state.totalPages = state.fb2TotalPages;
|
|
state.fb2CurrentPage = Math.round(progressToKeep * (state.fb2TotalPages - 1));
|
|
showFb2Page(state.fb2CurrentPage);
|
|
if (preservePosition && state.isBookLoaded) {
|
|
updateFb2Progress();
|
|
}
|
|
debugLog('FB2 pages: ' + state.fb2TotalPages);
|
|
if (typeof afterLayout === 'function') {
|
|
afterLayout();
|
|
}
|
|
});
|
|
}
|
|
|
|
function showFb2Page(idx) { // Переход на конкретную страницу FB2
|
|
idx = Math.max(0, Math.min(idx, state.fb2TotalPages - 1));
|
|
state.fb2CurrentPage = idx;
|
|
const inner = $('fb2-inner');
|
|
if (inner) {
|
|
// Сдвигаем контент влево, чтобы показать нужную "колонку"
|
|
inner.style.transform = `translateX(-${idx * els.fb2Content.clientWidth}px)`;
|
|
}
|
|
}
|
|
|
|
function updateFb2Progress() { // Оповещение приложения о прогрессе в FB2
|
|
const total = state.fb2TotalPages;
|
|
const progress = total > 1 ? state.fb2CurrentPage / (total - 1) : 0;
|
|
sendMessage('progressUpdate', {
|
|
progress,
|
|
cfi: progress.toString(),
|
|
currentPage: state.fb2CurrentPage + 1,
|
|
totalPages: total,
|
|
chapter: getCurrentFb2Chapter()
|
|
});
|
|
}
|
|
|
|
function getFb2ChapterAtOffset(offset) {
|
|
const inner = $('fb2-inner');
|
|
if (!inner) return '';
|
|
|
|
let chapter = '';
|
|
const titles = inner.querySelectorAll('.fb2-title');
|
|
for (let i = 0; i < titles.length; i++) {
|
|
if (titles[i].offsetLeft <= offset) chapter = titles[i].textContent;
|
|
else break;
|
|
}
|
|
return chapter;
|
|
}
|
|
|
|
function getCurrentFb2Chapter() { // Поиск заголовка, который сейчас виден на экране
|
|
const container = els.fb2Content;
|
|
if (!container) return '';
|
|
const currentOffset = state.fb2CurrentPage * container.clientWidth + 1;
|
|
return getFb2ChapterAtOffset(currentOffset);
|
|
}
|
|
|
|
function goToFb2Position(progress) { // Переход по проценту (0.0 - 1.0)
|
|
showFb2Page(Math.round(progress * (state.fb2TotalPages - 1)));
|
|
}
|
|
|
|
function sendBookSearchResults() {
|
|
sendMessage('bookSearchResults', {
|
|
query: state.search.query,
|
|
total: state.search.results.length,
|
|
currentIndex: state.search.currentIndex,
|
|
searching: state.search.searching
|
|
});
|
|
}
|
|
|
|
function makeSearchExcerpt(text, matchIndex, queryLength) {
|
|
const source = (text || '').replace(/\s+/g, ' ').trim();
|
|
if (!source) return '';
|
|
const start = Math.max(0, matchIndex - 48);
|
|
const end = Math.min(source.length, matchIndex + queryLength + 72);
|
|
const prefix = start > 0 ? '…' : '';
|
|
const suffix = end < source.length ? '…' : '';
|
|
return prefix + source.slice(start, end) + suffix;
|
|
}
|
|
|
|
function normalizeBookHref(href) {
|
|
return (href || '')
|
|
.split('#')[0]
|
|
.replace(/\\/g, '/')
|
|
.replace(/^(\.\.\/)+/, '')
|
|
.replace(/^\//, '');
|
|
}
|
|
|
|
function hrefsReferToSameSection(left, right) {
|
|
const a = normalizeBookHref(left);
|
|
const b = normalizeBookHref(right);
|
|
if (!a || !b) return false;
|
|
return a === b || a.endsWith('/' + b) || b.endsWith('/' + a);
|
|
}
|
|
|
|
function chapterForHref(href) {
|
|
if (!href) return '';
|
|
const normalized = normalizeBookHref(href);
|
|
const chapter = state.toc.find(item => hrefsReferToSameSection(href, item.href));
|
|
return chapter ? chapter.label : normalized;
|
|
}
|
|
|
|
function clearEpubSearchHighlights() {
|
|
if (!state.rendition || !state.rendition.annotations) return;
|
|
state.search.results.forEach(result => {
|
|
if (result.cfi) {
|
|
try {
|
|
state.rendition.annotations.remove(result.cfi, 'highlight');
|
|
} catch (e) {
|
|
debugLog('Search highlight remove failed: ' + e.message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function highlightEpubSearchResults(results) {
|
|
if (!state.rendition || !state.rendition.annotations) return;
|
|
results.forEach(result => {
|
|
if (result.cfi) {
|
|
try {
|
|
state.rendition.annotations.highlight(result.cfi, {}, null, 'book-search-highlight');
|
|
} catch (e) {
|
|
debugLog('Search highlight add failed: ' + e.message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function searchEpub(query, generation) {
|
|
const spine = (state.book && state.book.spine && (state.book.spine.spineItems || state.book.spine.items)) || [];
|
|
const results = [];
|
|
|
|
for (const section of spine) {
|
|
if (generation !== state.search.generation) return [];
|
|
try {
|
|
if (typeof section.load === 'function') {
|
|
await section.load(state.book.load.bind(state.book));
|
|
}
|
|
const matches = typeof section.find === 'function' ? section.find(query) : [];
|
|
matches.forEach(match => {
|
|
results.push({
|
|
cfi: match.cfi,
|
|
excerpt: match.excerpt || query,
|
|
href: section.href || '',
|
|
chapter: chapterForHref(section.href || '')
|
|
});
|
|
});
|
|
} catch (e) {
|
|
debugLog('EPUB search section failed: ' + e.message);
|
|
} finally {
|
|
if (typeof section.unload === 'function') {
|
|
section.unload();
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function searchFb2(query) {
|
|
const inner = $('fb2-inner');
|
|
if (!inner) return [];
|
|
|
|
const normalizedQuery = query.toLocaleLowerCase();
|
|
const totalTextLength = Math.max(1, inner.textContent.length);
|
|
const pageWidth = Math.max(1, els.fb2Content.clientWidth);
|
|
const totalPages = Math.max(1, state.fb2TotalPages);
|
|
const nodes = Array.from(inner.querySelectorAll('p,h1,h2,h3,h4,blockquote,li'));
|
|
const results = [];
|
|
let textOffset = 0;
|
|
|
|
nodes.forEach(node => {
|
|
const text = node.textContent || '';
|
|
const normalizedText = text.toLocaleLowerCase();
|
|
let matchIndex = normalizedText.indexOf(normalizedQuery);
|
|
while (matchIndex >= 0) {
|
|
const absoluteOffset = textOffset + matchIndex;
|
|
const pageByText = Math.floor((absoluteOffset / totalTextLength) * (totalPages - 1));
|
|
const pageByElement = node.offsetLeft > 0 ? Math.floor(node.offsetLeft / pageWidth) : pageByText;
|
|
const page = Math.max(0, Math.min(totalPages - 1, pageByElement));
|
|
const matchOffset = node.offsetLeft > 0 ? node.offsetLeft : page * pageWidth;
|
|
results.push({
|
|
page,
|
|
excerpt: makeSearchExcerpt(text, matchIndex, query.length),
|
|
chapter: getFb2ChapterAtOffset(matchOffset)
|
|
});
|
|
matchIndex = normalizedText.indexOf(normalizedQuery, matchIndex + normalizedQuery.length);
|
|
}
|
|
textOffset += text.length + 1;
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
async function searchBookInternal(rawQuery) {
|
|
const query = (rawQuery || '').trim();
|
|
clearEpubSearchHighlights();
|
|
const generation = state.search.generation + 1;
|
|
state.search = {
|
|
query,
|
|
results: [],
|
|
currentIndex: -1,
|
|
generation,
|
|
searching: query.length > 0
|
|
};
|
|
sendBookSearchResults();
|
|
|
|
if (!query) {
|
|
return;
|
|
}
|
|
|
|
const results = state.bookFormat === 'epub'
|
|
? await searchEpub(query, generation)
|
|
: searchFb2(query);
|
|
|
|
if (generation !== state.search.generation) {
|
|
return;
|
|
}
|
|
|
|
state.search.results = results;
|
|
state.search.searching = false;
|
|
state.search.currentIndex = results.length > 0 ? 0 : -1;
|
|
|
|
if (state.bookFormat === 'epub') {
|
|
highlightEpubSearchResults(results);
|
|
}
|
|
|
|
if (results.length > 0) {
|
|
showBookSearchResult(0);
|
|
} else {
|
|
sendBookSearchResults();
|
|
}
|
|
}
|
|
|
|
function showBookSearchResult(index) {
|
|
const results = state.search.results;
|
|
if (!results.length) {
|
|
sendBookSearchResults();
|
|
return;
|
|
}
|
|
|
|
const nextIndex = ((index % results.length) + results.length) % results.length;
|
|
state.search.currentIndex = nextIndex;
|
|
const result = results[nextIndex];
|
|
|
|
if (state.bookFormat === 'epub' && state.rendition && result.cfi) {
|
|
state.rendition.display(result.cfi);
|
|
} else if (state.bookFormat === 'fb2') {
|
|
showFb2Page(result.page || 0);
|
|
updateFb2Progress();
|
|
}
|
|
|
|
sendBookSearchResults();
|
|
}
|
|
|
|
// ========== PUBLIC API (Методы, доступные извне, например из C#) ==========
|
|
window.searchBook = function (query) {
|
|
searchBookInternal(query);
|
|
return 'started';
|
|
};
|
|
|
|
window.nextBookSearchResult = function () {
|
|
showBookSearchResult(state.search.currentIndex + 1);
|
|
};
|
|
|
|
window.previousBookSearchResult = function () {
|
|
showBookSearchResult(state.search.currentIndex - 1);
|
|
};
|
|
|
|
window.clearBookSearch = function () {
|
|
clearEpubSearchHighlights();
|
|
state.search = {
|
|
query: '',
|
|
results: [],
|
|
currentIndex: -1,
|
|
generation: state.search.generation + 1,
|
|
searching: false
|
|
};
|
|
sendBookSearchResults();
|
|
};
|
|
|
|
window.loadBookFromBase64 = function (base64Data, format, lastPosition, cachedLocations) {
|
|
window.clearBookSearch();
|
|
state.lastSelection = null;
|
|
state.isBookLoaded = false;
|
|
state.bookFormat = format;
|
|
if (format === 'epub') {
|
|
// Передаем кэш в основную функцию загрузки
|
|
loadEpubFromBase64(base64Data, lastPosition, cachedLocations);
|
|
}
|
|
else if (format === 'fb2') {
|
|
loadFb2FromBase64(base64Data, lastPosition);
|
|
}
|
|
else showError('Unsupported format: ' + format);
|
|
};
|
|
|
|
window.nextPage = function () { // Листать вперед
|
|
if (state.bookFormat === 'epub' && state.rendition) {
|
|
sendMessage('readerNavigation', {});
|
|
state.rendition.next();
|
|
}
|
|
else if (state.bookFormat === 'fb2' && state.fb2CurrentPage < state.fb2TotalPages - 1) {
|
|
showFb2Page(state.fb2CurrentPage + 1);
|
|
updateFb2Progress();
|
|
sendMessage('readerNavigation', {});
|
|
}
|
|
};
|
|
|
|
window.prevPage = function () { // Листать назад
|
|
if (state.bookFormat === 'epub' && state.rendition) {
|
|
sendMessage('readerNavigation', {});
|
|
state.rendition.prev();
|
|
}
|
|
else if (state.bookFormat === 'fb2' && state.fb2CurrentPage > 0) {
|
|
showFb2Page(state.fb2CurrentPage - 1);
|
|
updateFb2Progress();
|
|
sendMessage('readerNavigation', {});
|
|
}
|
|
};
|
|
|
|
window.goToProgress = function (value) {
|
|
const progress = Math.max(0, Math.min(1, Number(value) || 0));
|
|
if (state.bookFormat === 'epub' && state.rendition && state.book && state.book.locations.length() > 0) {
|
|
const target = state.book.locations.cfiFromPercentage(progress);
|
|
if (target) state.rendition.display(target);
|
|
}
|
|
else if (state.bookFormat === 'fb2') {
|
|
const targetPage = Math.round(progress * Math.max(0, state.fb2TotalPages - 1));
|
|
showFb2Page(targetPage);
|
|
updateFb2Progress();
|
|
}
|
|
};
|
|
|
|
function applyEpubReaderStyles(palette) {
|
|
if (!state.rendition) {
|
|
return;
|
|
}
|
|
|
|
state.rendition.themes.default({
|
|
'body': {
|
|
'font-family': state.currentFontFamily + ' !important',
|
|
'font-size': state.currentFontSize + 'px !important',
|
|
'line-height': '1.68 !important',
|
|
'padding': '28px 24px !important',
|
|
'background-color': palette.background + ' !important',
|
|
'color': palette.color + ' !important',
|
|
'hyphens': 'auto !important',
|
|
'overflow-wrap': 'break-word !important',
|
|
'-webkit-user-select': 'text !important',
|
|
'user-select': 'text !important',
|
|
'-webkit-touch-callout': 'default !important'
|
|
},
|
|
'p': { 'text-indent': '1.35em', 'margin-bottom': '0.75em' }
|
|
});
|
|
state.rendition.themes.fontSize(state.currentFontSize + 'px');
|
|
state.rendition.themes.font(state.currentFontFamily);
|
|
}
|
|
|
|
window.setFontSize = function (size) { // Изменение размера шрифта
|
|
state.currentFontSize = size;
|
|
if (state.bookFormat === 'epub' && state.rendition) {
|
|
applyEpubReaderStyles(getThemePalette(state.currentTheme));
|
|
scheduleReaderRelayout('font-size', true);
|
|
}
|
|
else if (state.bookFormat === 'fb2') {
|
|
const inner = $('fb2-inner');
|
|
if (inner) {
|
|
inner.style.fontSize = size + 'px';
|
|
scheduleReaderRelayout('font-size', true);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.setFontFamily = function (family) { // Изменение гарнитуры шрифта
|
|
state.currentFontFamily = family;
|
|
if (state.bookFormat === 'epub' && state.rendition) {
|
|
applyEpubReaderStyles(getThemePalette(state.currentTheme));
|
|
scheduleReaderRelayout('font-family', true);
|
|
}
|
|
else if (state.bookFormat === 'fb2') {
|
|
const inner = $('fb2-inner');
|
|
if (inner) {
|
|
inner.style.fontFamily = family;
|
|
scheduleReaderRelayout('font-family', true);
|
|
}
|
|
}
|
|
};
|
|
|
|
function getThemePalette(theme) {
|
|
switch (theme) {
|
|
case 'dark':
|
|
return { background: '#181411', color: '#F4E7D8' };
|
|
case 'light':
|
|
return { background: '#FFFDF8', color: '#2B1B15' };
|
|
default:
|
|
return { background: '#FAF1E2', color: '#33231B' };
|
|
}
|
|
}
|
|
|
|
function applyReaderTheme() {
|
|
const palette = getThemePalette(state.currentTheme);
|
|
document.body.style.backgroundColor = palette.background;
|
|
els.bookContent.style.backgroundColor = palette.background;
|
|
els.fb2Content.style.backgroundColor = palette.background;
|
|
|
|
applyEpubReaderStyles(palette);
|
|
|
|
const inner = $('fb2-inner');
|
|
if (inner) {
|
|
inner.style.backgroundColor = palette.background;
|
|
inner.style.color = palette.color;
|
|
}
|
|
}
|
|
|
|
function applyReaderBrightness() {
|
|
const brightness = Math.max(0.7, Math.min(1.2, state.brightness / 100));
|
|
els.bookContent.style.filter = `brightness(${brightness})`;
|
|
els.fb2Content.style.filter = `brightness(${brightness})`;
|
|
}
|
|
|
|
window.setReaderTheme = function (theme) {
|
|
state.currentTheme = theme || 'light';
|
|
applyReaderTheme();
|
|
};
|
|
|
|
window.setBrightness = function (value) {
|
|
state.brightness = value || 100;
|
|
applyReaderBrightness();
|
|
};
|
|
|
|
window.goToChapter = function (href) { // Переход к главе из оглавления
|
|
if (state.bookFormat === 'epub' && state.rendition) state.rendition.display(href);
|
|
else if (state.bookFormat === 'fb2') {
|
|
const inner = $('fb2-inner');
|
|
const el = inner
|
|
? Array.from(inner.querySelectorAll('[data-chapter]')).find(item => item.getAttribute('data-chapter') === href)
|
|
: null;
|
|
if (el) {
|
|
showFb2Page(Math.floor(el.offsetLeft / els.fb2Content.clientWidth));
|
|
updateFb2Progress();
|
|
}
|
|
}
|
|
};
|
|
|
|
window.goToPosition = function (position) {
|
|
if (!position) return;
|
|
if (state.bookFormat === 'epub' && state.rendition) {
|
|
state.rendition.display(position);
|
|
} else if (state.bookFormat === 'fb2') {
|
|
const progress = parseFloat(position);
|
|
if (!Number.isNaN(progress)) {
|
|
goToFb2Position(progress);
|
|
updateFb2Progress();
|
|
}
|
|
}
|
|
};
|
|
|
|
window.getProgress = function () { // Запрос текущего состояния прогресса (в JSON)
|
|
if (state.bookFormat === 'epub' && state.book && state.currentCfi) {
|
|
try {
|
|
return JSON.stringify({
|
|
progress: state.book.locations.percentageFromCfi(state.currentCfi) || 0,
|
|
cfi: state.currentCfi,
|
|
currentPage: state.currentPage,
|
|
totalPages: state.totalPages
|
|
});
|
|
} catch (e) { return '{}'; }
|
|
} else if (state.bookFormat === 'fb2') {
|
|
const p = state.fb2TotalPages > 1 ? state.fb2CurrentPage / (state.fb2TotalPages - 1) : 0;
|
|
return JSON.stringify({
|
|
progress: p, cfi: p.toString(),
|
|
currentPage: state.fb2CurrentPage + 1, totalPages: state.fb2TotalPages
|
|
});
|
|
}
|
|
return '{}';
|
|
};
|
|
|
|
window.getSelectionSnapshot = function () {
|
|
if (state.bookFormat === 'epub' && state.book) {
|
|
const selection = state.lastSelection || {};
|
|
const cfi = selection.cfi || state.currentCfi || state.lastCfi || '';
|
|
let progress = 0;
|
|
try {
|
|
progress = cfi ? (state.book.locations.percentageFromCfi(cfi) || 0) : 0;
|
|
} catch (e) {
|
|
progress = 0;
|
|
}
|
|
return JSON.stringify({
|
|
selectedText: selection.text || '',
|
|
progress,
|
|
cfi,
|
|
currentPage: state.currentPage || Math.round(progress * 100),
|
|
totalPages: state.totalPages || 100
|
|
});
|
|
}
|
|
|
|
if (state.bookFormat === 'fb2') {
|
|
const p = state.fb2TotalPages > 1 ? state.fb2CurrentPage / (state.fb2TotalPages - 1) : 0;
|
|
let selectedText = '';
|
|
try {
|
|
selectedText = window.getSelection ? window.getSelection().toString() : '';
|
|
} catch (e) {
|
|
selectedText = '';
|
|
}
|
|
return JSON.stringify({
|
|
selectedText,
|
|
progress: p,
|
|
cfi: p.toString(),
|
|
currentPage: state.fb2CurrentPage + 1,
|
|
totalPages: state.fb2TotalPages,
|
|
chapter: getCurrentFb2Chapter()
|
|
});
|
|
}
|
|
|
|
return '{}';
|
|
};
|
|
|
|
// ========== РРќРР¦РРђР›РР—РђР¦РРЇ ==========
|
|
initReaderGestures();
|
|
initReaderResizeHandling();
|
|
setLoadingText('Ожидаю файл книги...'); // Сообщаем, что готовы принимать файл
|
|
sendMessage('readerReady', {}); // Уведомляем нативное приложение: "Я загрузился!"
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|
|
|