edit
This commit is contained in:
@@ -29,7 +29,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#book-content {
|
#book-content, #fb2-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -38,8 +38,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#fb2-content {
|
#fb2-content {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -58,11 +56,11 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading .spinner {
|
.spinner {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 4px solid #ddd;
|
border: 4px solid #ddd;
|
||||||
border-top: 4px solid #5D4037;
|
border-top-color: #5D4037;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -86,23 +84,8 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#debug-log {
|
/* Прозрачные зоны для тачей */
|
||||||
position: fixed;
|
.touch-zone {
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: rgba(0,0,0,0.9);
|
|
||||||
color: #0f0;
|
|
||||||
font-size: 11px;
|
|
||||||
font-family: monospace;
|
|
||||||
padding: 5px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
/* Прозрачные зоны для тачей поверх всего */
|
|
||||||
#touch-left, #touch-right, #touch-center {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -138,166 +121,154 @@
|
|||||||
<span id="error-text"></span>
|
<span id="error-text"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Невидимые зоны тачей поверх iframe epub.js -->
|
<div id="touch-left" class="touch-zone"></div>
|
||||||
<div id="touch-left"></div>
|
<div id="touch-center" class="touch-zone"></div>
|
||||||
<div id="touch-center"></div>
|
<div id="touch-right" class="touch-zone"></div>
|
||||||
<div id="touch-right"></div>
|
|
||||||
<script src="_framework/hybridwebview.js"></script>
|
<script src="_framework/hybridwebview.js"></script>
|
||||||
|
<script src="js/jszip.min.js"></script>
|
||||||
|
<script src="js/epub.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ========== DEBUG LOGGING ==========
|
(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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== СОСТОЯНИЕ ==========
|
||||||
|
const state = {
|
||||||
|
book: null,
|
||||||
|
rendition: null,
|
||||||
|
currentCfi: null,
|
||||||
|
totalPages: 0,
|
||||||
|
currentPage: 0,
|
||||||
|
bookFormat: '',
|
||||||
|
isBookLoaded: false,
|
||||||
|
fb2CurrentPage: 0,
|
||||||
|
fb2TotalPages: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== УТИЛИТЫ ==========
|
||||||
function debugLog(msg) {
|
function debugLog(msg) {
|
||||||
console.log('[Reader] ' + msg);
|
console.log('[Reader] ' + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
document.getElementById('loading').style.display = 'none';
|
els.loading.style.display = 'none';
|
||||||
document.getElementById('error-display').style.display = 'flex';
|
els.errorDisplay.style.display = 'flex';
|
||||||
document.getElementById('error-text').textContent = msg;
|
els.errorText.textContent = msg;
|
||||||
debugLog('ERROR: ' + msg);
|
debugLog('ERROR: ' + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoadingText(msg) {
|
function setLoadingText(msg) {
|
||||||
var el = document.getElementById('loading-text');
|
if (els.loadingText) els.loadingText.textContent = msg;
|
||||||
if (el) el.textContent = msg;
|
|
||||||
debugLog(msg);
|
debugLog(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Один div для escapeHtml — переиспользуем
|
||||||
|
const _escDiv = document.createElement('div');
|
||||||
|
function escapeHtml(text) {
|
||||||
|
_escDiv.textContent = text;
|
||||||
|
return _escDiv.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToArrayBuffer(base64) {
|
||||||
|
const bin = atob(base64);
|
||||||
|
const len = bin.length;
|
||||||
|
const buf = new ArrayBuffer(len);
|
||||||
|
const view = new Uint8Array(buf);
|
||||||
|
// Обработка блоками для больших файлов — снижает давление на GC
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
view[i] = bin.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== MESSAGE BRIDGE ==========
|
// ========== MESSAGE BRIDGE ==========
|
||||||
function sendMessage(action, data) {
|
function sendMessage(action, data) {
|
||||||
var message = JSON.stringify({ action: action, data: data || {} });
|
const message = JSON.stringify({ action, data: data || {} });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// .NET MAUI HybridWebView uses hybridWebViewHost
|
|
||||||
if (window.HybridWebView && typeof window.HybridWebView.SendRawMessage === 'function') {
|
if (window.HybridWebView && typeof window.HybridWebView.SendRawMessage === 'function') {
|
||||||
window.HybridWebView.SendRawMessage(message);
|
window.HybridWebView.SendRawMessage(message);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('No bridge method found on hybridWebViewHost');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog('Bridge error: ' + e.message);
|
debugLog('Bridge error: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== TOUCH ZONES ==========
|
// ========== TOUCH / SWIPE ==========
|
||||||
document.getElementById('touch-left').addEventListener('click', function (e) {
|
let swipeStartX = 0, swipeStartY = 0, swipeStartTime = 0;
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
prevPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('touch-right').addEventListener('click', function (e) {
|
const touchActions = {
|
||||||
e.preventDefault();
|
'touch-left': () => window.prevPage(),
|
||||||
e.stopPropagation();
|
'touch-right': () => window.nextPage(),
|
||||||
nextPage();
|
'touch-center': () => sendMessage('toggleMenu', {}),
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('touch-center').addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
sendMessage('toggleMenu', {});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Свайпы на зонах
|
|
||||||
var swipeStartX = 0, swipeStartY = 0, swipeStartTime = 0;
|
|
||||||
|
|
||||||
function onSwipeStart(e) {
|
|
||||||
var touch = e.touches ? e.touches[0] : e;
|
|
||||||
swipeStartX = touch.clientX;
|
|
||||||
swipeStartY = touch.clientY;
|
|
||||||
swipeStartTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSwipeEnd(e) {
|
|
||||||
var touch = e.changedTouches ? e.changedTouches[0] : e;
|
|
||||||
var dx = touch.clientX - swipeStartX;
|
|
||||||
var dy = touch.clientY - swipeStartY;
|
|
||||||
var dt = Date.now() - swipeStartTime;
|
|
||||||
|
|
||||||
if (dt > 500) return;
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
|
|
||||||
if (dx < 0) nextPage();
|
|
||||||
else prevPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
['touch-left', 'touch-center', 'touch-right'].forEach(function (id) {
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
el.addEventListener('touchstart', onSwipeStart, { passive: true });
|
|
||||||
el.addEventListener('touchend', onSwipeEnd, { passive: true });
|
|
||||||
});
|
|
||||||
// ========== EPUB READER ==========
|
|
||||||
var book = null;
|
|
||||||
var rendition = null;
|
|
||||||
var currentCfi = null;
|
|
||||||
var totalPages = 0;
|
|
||||||
var currentPage = 0;
|
|
||||||
var bookFormat = '';
|
|
||||||
var isBookLoaded = false;
|
|
||||||
|
|
||||||
window.loadBookFromBase64 = function (base64Data, format, lastPosition) {
|
|
||||||
debugLog('loadBookFromBase64: format=' + format + ', len=' + (base64Data ? base64Data.length : 0));
|
|
||||||
bookFormat = format;
|
|
||||||
|
|
||||||
if (format === 'epub') {
|
|
||||||
loadEpubFromBase64(base64Data, lastPosition);
|
|
||||||
} else if (format === 'fb2') {
|
|
||||||
loadFb2FromBase64(base64Data, lastPosition);
|
|
||||||
} else {
|
|
||||||
showError('Unsupported format: ' + format);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64) {
|
function initTouchZones() {
|
||||||
var binaryString = atob(base64);
|
Object.keys(touchActions).forEach(id => {
|
||||||
var len = binaryString.length;
|
const el = $(id);
|
||||||
var bytes = new Uint8Array(len);
|
if (!el) return;
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
el.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
touchActions[id]();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('touchstart', e => {
|
||||||
|
const t = e.touches[0];
|
||||||
|
swipeStartX = t.clientX;
|
||||||
|
swipeStartY = t.clientY;
|
||||||
|
swipeStartTime = Date.now();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
el.addEventListener('touchend', e => {
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
const dx = t.clientX - swipeStartX;
|
||||||
|
const dt = Date.now() - swipeStartTime;
|
||||||
|
if (dt < 500 && Math.abs(dx) > Math.abs(t.clientY - swipeStartY) && Math.abs(dx) > 50) {
|
||||||
|
dx < 0 ? window.nextPage() : window.prevPage();
|
||||||
}
|
}
|
||||||
return bytes.buffer;
|
}, { passive: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== EPUB ==========
|
||||||
function loadEpubFromBase64(base64Data, lastCfi) {
|
function loadEpubFromBase64(base64Data, lastCfi) {
|
||||||
setLoadingText('Decoding EPUB...');
|
setLoadingText('Decoding EPUB...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var arrayBuffer = base64ToArrayBuffer(base64Data);
|
const arrayBuffer = base64ToArrayBuffer(base64Data);
|
||||||
debugLog('EPUB size: ' + arrayBuffer.byteLength + ' bytes');
|
debugLog('EPUB size: ' + arrayBuffer.byteLength + ' bytes');
|
||||||
|
|
||||||
// Check JSZip
|
if (typeof JSZip === 'undefined') { showError('JSZip not loaded!'); return; }
|
||||||
if (typeof JSZip === 'undefined') {
|
if (typeof ePub === 'undefined') { showError('epub.js not loaded!'); return; }
|
||||||
showError('JSZip not loaded! Cannot open EPUB.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
debugLog('JSZip version: ' + (JSZip.version || 'unknown'));
|
|
||||||
|
|
||||||
// Check epub.js
|
|
||||||
if (typeof ePub === 'undefined') {
|
|
||||||
showError('epub.js not loaded!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
debugLog('ePub function available');
|
|
||||||
|
|
||||||
setLoadingText('Opening EPUB...');
|
setLoadingText('Opening EPUB...');
|
||||||
|
|
||||||
// First unzip manually to verify the file is valid
|
// epub.js сам умеет работать с ArrayBuffer — двойная распаковка не нужна
|
||||||
JSZip.loadAsync(arrayBuffer).then(function (zip) {
|
state.book = ePub(arrayBuffer);
|
||||||
debugLog('ZIP opened, files: ' + Object.keys(zip.files).length);
|
|
||||||
|
|
||||||
// Now use epub.js
|
els.fb2Content.style.display = 'none';
|
||||||
book = ePub(arrayBuffer);
|
els.bookContent.style.display = 'block';
|
||||||
|
|
||||||
document.getElementById('fb2-content').style.display = 'none';
|
state.rendition = state.book.renderTo('book-content', {
|
||||||
document.getElementById('book-content').style.display = 'block';
|
|
||||||
|
|
||||||
rendition = book.renderTo('book-content', {
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
spread: 'none',
|
spread: 'none',
|
||||||
flow: 'paginated'
|
flow: 'paginated'
|
||||||
});
|
});
|
||||||
rendition.themes.default({
|
|
||||||
|
state.rendition.themes.default({
|
||||||
'body': {
|
'body': {
|
||||||
'font-family': 'serif !important',
|
'font-family': 'serif !important',
|
||||||
'font-size': '18px !important',
|
'font-size': '18px !important',
|
||||||
@@ -306,129 +277,110 @@
|
|||||||
'background-color': '#faf8ef !important',
|
'background-color': '#faf8ef !important',
|
||||||
'color': '#333 !important'
|
'color': '#333 !important'
|
||||||
},
|
},
|
||||||
'p': {
|
'p': { 'text-indent': '1.5em', 'margin-bottom': '0.5em' }
|
||||||
'text-indent': '1.5em',
|
|
||||||
'margin-bottom': '0.5em'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
book.ready.then(function () {
|
state.book.ready
|
||||||
|
.then(() => {
|
||||||
debugLog('EPUB ready');
|
debugLog('EPUB ready');
|
||||||
document.getElementById('loading').style.display = 'none';
|
els.loading.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var toc = book.navigation.toc || [];
|
const toc = state.book.navigation.toc || [];
|
||||||
var chapters = toc.map(function (ch) {
|
const chapters = toc.map(ch => ({
|
||||||
return { label: (ch.label || '').trim(), href: ch.href || '' };
|
label: (ch.label || '').trim(),
|
||||||
});
|
href: ch.href || ''
|
||||||
sendMessage('chaptersLoaded', { chapters: chapters });
|
}));
|
||||||
debugLog('TOC chapters: ' + chapters.length);
|
sendMessage('chaptersLoaded', { chapters });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog('TOC error: ' + e.message);
|
debugLog('TOC error: ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return book.locations.generate(1600);
|
return state.book.locations.generate(1600);
|
||||||
}).then(function () {
|
})
|
||||||
totalPages = book.locations.length();
|
.then(() => {
|
||||||
debugLog('Pages: ' + totalPages);
|
state.totalPages = state.book.locations.length();
|
||||||
sendMessage('bookReady', { totalPages: totalPages });
|
debugLog('Pages: ' + state.totalPages);
|
||||||
isBookLoaded = true;
|
sendMessage('bookReady', { totalPages: state.totalPages });
|
||||||
}).catch(function (e) {
|
state.isBookLoaded = true;
|
||||||
debugLog('Book ready error: ' + e.message);
|
})
|
||||||
});
|
.catch(e => debugLog('Book ready error: ' + e.message));
|
||||||
|
|
||||||
rendition.on('relocated', function (location) {
|
state.rendition.on('relocated', location => {
|
||||||
if (!location || !location.start) return;
|
if (!location || !location.start) return;
|
||||||
currentCfi = location.start.cfi;
|
state.currentCfi = location.start.cfi;
|
||||||
try {
|
try {
|
||||||
var progress = book.locations.percentageFromCfi(currentCfi) || 0;
|
|
||||||
currentPage = location.start.location || 0;
|
|
||||||
sendMessage('progressUpdate', {
|
sendMessage('progressUpdate', {
|
||||||
progress: progress,
|
progress: state.book.locations.percentageFromCfi(state.currentCfi) || 0,
|
||||||
cfi: currentCfi,
|
cfi: state.currentCfi,
|
||||||
currentPage: currentPage,
|
currentPage: location.start.location || 0,
|
||||||
totalPages: totalPages,
|
totalPages: state.totalPages,
|
||||||
chapter: location.start.href || ''
|
chapter: location.start.href || ''
|
||||||
});
|
});
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
debugLog('Relocated error: ' + e.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoadingText('Rendering...');
|
setLoadingText('Rendering...');
|
||||||
|
const displayTarget = (lastCfi && lastCfi !== 'null' && lastCfi !== 'undefined')
|
||||||
if (lastCfi && lastCfi.length > 0 && lastCfi !== 'null' && lastCfi !== 'undefined') {
|
? lastCfi : undefined;
|
||||||
rendition.display(lastCfi);
|
state.rendition.display(displayTarget);
|
||||||
} else {
|
|
||||||
rendition.display();
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch(function (e) {
|
|
||||||
showError('ZIP error: ' + e.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError('EPUB load error: ' + e.message);
|
showError('EPUB load error: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== FB2 ==========
|
||||||
function loadFb2FromBase64(base64Data, lastPosition) {
|
function loadFb2FromBase64(base64Data, lastPosition) {
|
||||||
setLoadingText('Parsing FB2...');
|
setLoadingText('Parsing FB2...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var arrayBuffer = base64ToArrayBuffer(base64Data);
|
const arrayBuffer = base64ToArrayBuffer(base64Data);
|
||||||
var bytes = new Uint8Array(arrayBuffer);
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
var xmlText;
|
let xmlText = new TextDecoder('utf-8').decode(bytes);
|
||||||
try {
|
|
||||||
xmlText = new TextDecoder('utf-8').decode(bytes);
|
|
||||||
} catch (e) {
|
|
||||||
xmlText = new TextDecoder('windows-1251').decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check encoding declaration and re-decode if needed
|
// Перекодируем если нужно
|
||||||
var encodingMatch = xmlText.match(/encoding=["\']([^"\']+)["\']/i);
|
const encMatch = xmlText.match(/encoding=["\']([^"\']+)["\']/i);
|
||||||
if (encodingMatch) {
|
if (encMatch) {
|
||||||
var declaredEncoding = encodingMatch[1].toLowerCase();
|
const enc = encMatch[1].toLowerCase();
|
||||||
debugLog('FB2 encoding: ' + declaredEncoding);
|
debugLog('FB2 encoding: ' + enc);
|
||||||
if (declaredEncoding !== 'utf-8') {
|
if (enc !== 'utf-8') {
|
||||||
try {
|
try { xmlText = new TextDecoder(enc).decode(bytes); }
|
||||||
xmlText = new TextDecoder(declaredEncoding).decode(bytes);
|
catch (e) { debugLog('Re-decode error: ' + e.message); }
|
||||||
} catch (e) {
|
|
||||||
debugLog('Re-decode error: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('book-content').style.display = 'none';
|
els.bookContent.style.display = 'none';
|
||||||
document.getElementById('fb2-content').style.display = 'block';
|
els.fb2Content.style.display = 'block';
|
||||||
|
|
||||||
var parser = new DOMParser();
|
const doc = new DOMParser().parseFromString(xmlText, 'text/xml');
|
||||||
var doc = parser.parseFromString(xmlText, 'text/xml');
|
if (doc.querySelector('parsererror')) {
|
||||||
|
|
||||||
var parseError = doc.querySelector('parsererror');
|
|
||||||
if (parseError) {
|
|
||||||
showError('FB2 XML parse error');
|
showError('FB2 XML parse error');
|
||||||
debugLog(parseError.textContent.substring(0, 200));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fb2Html = parseFb2Document(doc);
|
const fb2Html = parseFb2Document(doc);
|
||||||
var fb2Container = document.getElementById('fb2-content');
|
els.fb2Content.innerHTML = fb2Html.html;
|
||||||
fb2Container.innerHTML = fb2Html.html;
|
els.loading.style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
// Используем requestAnimationFrame вместо setTimeout для точного тайминга
|
||||||
|
requestAnimationFrame(() => {
|
||||||
setTimeout(function () {
|
requestAnimationFrame(() => {
|
||||||
setupFb2Pagination();
|
setupFb2Pagination();
|
||||||
sendMessage('chaptersLoaded', { chapters: fb2Html.chapters });
|
sendMessage('chaptersLoaded', { chapters: fb2Html.chapters });
|
||||||
sendMessage('bookReady', { totalPages: totalPages });
|
sendMessage('bookReady', { totalPages: state.totalPages });
|
||||||
isBookLoaded = true;
|
state.isBookLoaded = true;
|
||||||
bookFormat = 'fb2';
|
state.bookFormat = 'fb2';
|
||||||
|
|
||||||
if (lastPosition && parseFloat(lastPosition) > 0) {
|
if (lastPosition && parseFloat(lastPosition) > 0) {
|
||||||
goToFb2Position(parseFloat(lastPosition));
|
goToFb2Position(parseFloat(lastPosition));
|
||||||
}
|
}
|
||||||
updateFb2Progress();
|
updateFb2Progress();
|
||||||
}, 500);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError('FB2 error: ' + e.message);
|
showError('FB2 error: ' + e.message);
|
||||||
@@ -436,299 +388,289 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== FB2 PARSER ==========
|
// ========== FB2 PARSER ==========
|
||||||
function parseFb2Document(doc) {
|
// Вспомогательная: получить локальное имя тега без namespace
|
||||||
var chapters = [];
|
function localTag(el) {
|
||||||
var html = '<div id="fb2-inner" style="font-size:18px; font-family:serif; line-height:1.6;">';
|
return el.tagName ? el.tagName.toLowerCase().replace(/.*:/, '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
var bodies = doc.querySelectorAll('body');
|
function parseFb2Document(doc) {
|
||||||
if (bodies.length === 0) {
|
const chapters = [];
|
||||||
|
const parts = ['<div id="fb2-inner" style="font-size:18px;font-family:serif;line-height:1.6;">'];
|
||||||
|
|
||||||
|
let bodies = doc.querySelectorAll('body');
|
||||||
|
if (!bodies.length) {
|
||||||
bodies = doc.getElementsByTagNameNS('http://www.gribuser.ru/xml/fictionbook/2.0', 'body');
|
bodies = doc.getElementsByTagNameNS('http://www.gribuser.ru/xml/fictionbook/2.0', 'body');
|
||||||
}
|
}
|
||||||
|
|
||||||
var chapterIndex = 0;
|
let chapterIndex = 0;
|
||||||
|
for (const body of bodies) {
|
||||||
for (var b = 0; b < bodies.length; b++) {
|
for (const child of body.children) {
|
||||||
var children = bodies[b].children;
|
if (localTag(child) === 'section') {
|
||||||
for (var s = 0; s < children.length; s++) {
|
const result = parseFb2Section(child, chapterIndex);
|
||||||
var child = children[s];
|
parts.push(result.html);
|
||||||
var tagName = child.tagName ? child.tagName.toLowerCase().replace(/.*:/, '') : '';
|
chapters.push(...result.chapters);
|
||||||
if (tagName === 'section') {
|
|
||||||
var result = parseFb2Section(child, chapterIndex);
|
|
||||||
html += result.html;
|
|
||||||
for (var c = 0; c < result.chapters.length; c++) {
|
|
||||||
chapters.push(result.chapters[c]);
|
|
||||||
}
|
|
||||||
chapterIndex += Math.max(result.chapters.length, 1);
|
chapterIndex += Math.max(result.chapters.length, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
parts.push('</div>');
|
||||||
if (chapters.length === 0) chapters.push({ label: 'Start', href: '0' });
|
if (!chapters.length) chapters.push({ label: 'Start', href: '0' });
|
||||||
return { html: html, chapters: chapters };
|
return { html: parts.join(''), chapters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Маппинг тегов FB2 → генераторы HTML
|
||||||
|
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) {
|
function parseFb2Section(section, startIndex) {
|
||||||
var chapters = [];
|
const chapters = [];
|
||||||
var html = '<div class="fb2-section" data-section="' + startIndex + '">';
|
const parts = [`<div class="fb2-section" data-section="${startIndex}">`];
|
||||||
|
|
||||||
var children = section.children;
|
for (const child of section.children) {
|
||||||
for (var i = 0; i < children.length; i++) {
|
const tag = localTag(child);
|
||||||
var child = children[i];
|
|
||||||
var tag = child.tagName ? child.tagName.toLowerCase().replace(/.*:/, '') : '';
|
|
||||||
|
|
||||||
switch (tag) {
|
if (tag === 'section') {
|
||||||
case 'title':
|
const sub = parseFb2Section(child, startIndex + chapters.length);
|
||||||
var titleText = (child.textContent || '').trim();
|
parts.push(sub.html);
|
||||||
html += '<h2 class="fb2-title" data-chapter="' + startIndex + '" style="text-align:center; margin:1em 0 0.5em;">' + escapeHtml(titleText) + '</h2>';
|
chapters.push(...sub.chapters);
|
||||||
chapters.push({ label: titleText, href: startIndex.toString() });
|
} else if (sectionTagHandlers[tag]) {
|
||||||
break;
|
parts.push(sectionTagHandlers[tag](child, startIndex + chapters.length, chapters));
|
||||||
case 'p':
|
|
||||||
html += '<p style="text-indent:1.5em; margin-bottom:0.3em;">' + getInlineHtml(child) + '</p>';
|
|
||||||
break;
|
|
||||||
case 'empty-line':
|
|
||||||
html += '<br/>';
|
|
||||||
break;
|
|
||||||
case 'subtitle':
|
|
||||||
html += '<h3 style="text-align:center; margin:0.8em 0;">' + escapeHtml(child.textContent || '') + '</h3>';
|
|
||||||
break;
|
|
||||||
case 'epigraph':
|
|
||||||
html += '<blockquote style="font-style:italic; margin:1em 2em;">' + parseInnerParagraphs(child) + '</blockquote>';
|
|
||||||
break;
|
|
||||||
case 'poem':
|
|
||||||
html += '<div style="margin:1em 2em;">' + parsePoem(child) + '</div>';
|
|
||||||
break;
|
|
||||||
case 'cite':
|
|
||||||
html += '<blockquote style="margin:1em 2em; padding-left:1em; border-left:3px solid #ccc;">' + parseInnerParagraphs(child) + '</blockquote>';
|
|
||||||
break;
|
|
||||||
case 'section':
|
|
||||||
var sub = parseFb2Section(child, startIndex + chapters.length);
|
|
||||||
html += sub.html;
|
|
||||||
for (var j = 0; j < sub.chapters.length; j++) chapters.push(sub.chapters[j]);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
parts.push('</div>');
|
||||||
return { html: html, chapters: chapters };
|
return { html: parts.join(''), chapters };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInlineHtml(el) {
|
function getInlineHtml(el) {
|
||||||
var result = '';
|
const parts = [];
|
||||||
for (var i = 0; i < el.childNodes.length; i++) {
|
for (const node of el.childNodes) {
|
||||||
var node = el.childNodes[i];
|
|
||||||
if (node.nodeType === 3) {
|
if (node.nodeType === 3) {
|
||||||
result += escapeHtml(node.textContent);
|
parts.push(escapeHtml(node.textContent));
|
||||||
} else if (node.nodeType === 1) {
|
} else if (node.nodeType === 1) {
|
||||||
var tag = node.tagName.toLowerCase().replace(/.*:/, '');
|
const tag = localTag(node);
|
||||||
if (tag === 'strong' || tag === 'bold') result += '<strong>' + getInlineHtml(node) + '</strong>';
|
const inner = getInlineHtml(node);
|
||||||
else if (tag === 'emphasis' || tag === 'em') result += '<em>' + getInlineHtml(node) + '</em>';
|
if (tag === 'strong' || tag === 'bold') parts.push(`<strong>${inner}</strong>`);
|
||||||
else if (tag === 'strikethrough') result += '<s>' + getInlineHtml(node) + '</s>';
|
else if (tag === 'emphasis' || tag === 'em') parts.push(`<em>${inner}</em>`);
|
||||||
else result += getInlineHtml(node);
|
else if (tag === 'strikethrough') parts.push(`<s>${inner}</s>`);
|
||||||
|
else parts.push(inner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return parts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInnerParagraphs(el) {
|
function parseInnerParagraphs(el) {
|
||||||
var html = '';
|
const parts = [];
|
||||||
for (var i = 0; i < el.children.length; i++) {
|
for (const child of el.children) {
|
||||||
var child = el.children[i];
|
const tag = localTag(child);
|
||||||
var tag = child.tagName ? child.tagName.toLowerCase().replace(/.*:/, '') : '';
|
if (tag === 'p') parts.push(`<p>${getInlineHtml(child)}</p>`);
|
||||||
if (tag === 'p') html += '<p>' + getInlineHtml(child) + '</p>';
|
else if (tag === 'text-author') parts.push(`<p style="text-align:right;font-style:italic">— ${escapeHtml(child.textContent || '')}</p>`);
|
||||||
else if (tag === 'text-author') html += '<p style="text-align:right; font-style:italic;">— ' + escapeHtml(child.textContent || '') + '</p>';
|
|
||||||
}
|
}
|
||||||
return html;
|
return parts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePoem(el) {
|
function parsePoem(el) {
|
||||||
var html = '';
|
const parts = [];
|
||||||
for (var i = 0; i < el.children.length; i++) {
|
for (const child of el.children) {
|
||||||
var child = el.children[i];
|
const tag = localTag(child);
|
||||||
var tag = child.tagName ? child.tagName.toLowerCase().replace(/.*:/, '') : '';
|
|
||||||
if (tag === 'stanza') {
|
if (tag === 'stanza') {
|
||||||
html += '<div style="margin-bottom:1em;">';
|
parts.push('<div style="margin-bottom:1em">');
|
||||||
for (var j = 0; j < child.children.length; j++) {
|
for (const v of child.children) {
|
||||||
var v = child.children[j];
|
if (localTag(v) === 'v') parts.push(`<p style="text-indent:0">${escapeHtml(v.textContent || '')}</p>`);
|
||||||
if (v.tagName && v.tagName.toLowerCase().replace(/.*:/, '') === 'v')
|
|
||||||
html += '<p style="text-indent:0;">' + escapeHtml(v.textContent || '') + '</p>';
|
|
||||||
}
|
}
|
||||||
html += '</div>';
|
parts.push('</div>');
|
||||||
} else if (tag === 'title') {
|
} else if (tag === 'title') {
|
||||||
html += '<h4>' + escapeHtml(child.textContent || '') + '</h4>';
|
parts.push(`<h4>${escapeHtml(child.textContent || '')}</h4>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html;
|
return parts.join('');
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== FB2 PAGINATION ==========
|
// ========== FB2 PAGINATION ==========
|
||||||
var fb2CurrentPage = 0;
|
|
||||||
var fb2TotalPages = 1;
|
|
||||||
|
|
||||||
function setupFb2Pagination() {
|
function setupFb2Pagination() {
|
||||||
var container = document.getElementById('fb2-content');
|
const container = els.fb2Content;
|
||||||
var inner = document.getElementById('fb2-inner');
|
const inner = $('fb2-inner');
|
||||||
if (!container || !inner) return;
|
if (!container || !inner) return;
|
||||||
|
|
||||||
var w = container.clientWidth;
|
const w = container.clientWidth;
|
||||||
var h = container.clientHeight;
|
const h = container.clientHeight;
|
||||||
|
|
||||||
inner.style.columnWidth = w + 'px';
|
Object.assign(inner.style, {
|
||||||
inner.style.columnGap = '40px';
|
columnWidth: w + 'px',
|
||||||
inner.style.columnFill = 'auto';
|
columnGap: '40px',
|
||||||
inner.style.height = h + 'px';
|
columnFill: 'auto',
|
||||||
inner.style.overflow = 'hidden';
|
height: h + 'px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(function () {
|
// Даём браузеру отрисовать колонки
|
||||||
fb2TotalPages = Math.max(1, Math.ceil(inner.scrollWidth / w));
|
requestAnimationFrame(() => {
|
||||||
totalPages = fb2TotalPages;
|
state.fb2TotalPages = Math.max(1, Math.ceil(inner.scrollWidth / w));
|
||||||
fb2CurrentPage = 0;
|
state.totalPages = state.fb2TotalPages;
|
||||||
|
state.fb2CurrentPage = 0;
|
||||||
showFb2Page(0);
|
showFb2Page(0);
|
||||||
debugLog('FB2 pages: ' + fb2TotalPages);
|
debugLog('FB2 pages: ' + state.fb2TotalPages);
|
||||||
}, 300);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFb2Page(idx) {
|
function showFb2Page(idx) {
|
||||||
if (idx < 0) idx = 0;
|
idx = Math.max(0, Math.min(idx, state.fb2TotalPages - 1));
|
||||||
if (idx >= fb2TotalPages) idx = fb2TotalPages - 1;
|
state.fb2CurrentPage = idx;
|
||||||
fb2CurrentPage = idx;
|
const inner = $('fb2-inner');
|
||||||
var inner = document.getElementById('fb2-inner');
|
if (inner) {
|
||||||
var container = document.getElementById('fb2-content');
|
inner.style.transform = `translateX(-${idx * els.fb2Content.clientWidth}px)`;
|
||||||
if (inner && container) {
|
|
||||||
inner.style.transform = 'translateX(-' + (idx * container.clientWidth) + 'px)';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFb2Progress() {
|
function updateFb2Progress() {
|
||||||
var progress = fb2TotalPages > 1 ? fb2CurrentPage / (fb2TotalPages - 1) : 0;
|
const total = state.fb2TotalPages;
|
||||||
|
const progress = total > 1 ? state.fb2CurrentPage / (total - 1) : 0;
|
||||||
sendMessage('progressUpdate', {
|
sendMessage('progressUpdate', {
|
||||||
progress: progress,
|
progress,
|
||||||
cfi: progress.toString(),
|
cfi: progress.toString(),
|
||||||
currentPage: fb2CurrentPage + 1,
|
currentPage: state.fb2CurrentPage + 1,
|
||||||
totalPages: fb2TotalPages,
|
totalPages: total,
|
||||||
chapter: getCurrentFb2Chapter()
|
chapter: getCurrentFb2Chapter()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentFb2Chapter() {
|
function getCurrentFb2Chapter() {
|
||||||
var inner = document.getElementById('fb2-inner');
|
const inner = $('fb2-inner');
|
||||||
var container = document.getElementById('fb2-content');
|
const container = els.fb2Content;
|
||||||
if (!inner || !container) return '';
|
if (!inner || !container) return '';
|
||||||
var offset = fb2CurrentPage * container.clientWidth;
|
|
||||||
var ch = '';
|
const maxOffset = state.fb2CurrentPage * container.clientWidth + container.clientWidth;
|
||||||
var titles = inner.querySelectorAll('.fb2-title');
|
let chapter = '';
|
||||||
for (var i = 0; i < titles.length; i++) {
|
const titles = inner.querySelectorAll('.fb2-title');
|
||||||
if (titles[i].offsetLeft <= offset + container.clientWidth) ch = titles[i].textContent;
|
for (let i = 0; i < titles.length; i++) {
|
||||||
|
if (titles[i].offsetLeft <= maxOffset) chapter = titles[i].textContent;
|
||||||
|
else break; // Заголовки идут по порядку — можно прервать
|
||||||
}
|
}
|
||||||
return ch;
|
return chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToFb2Position(progress) {
|
function goToFb2Position(progress) {
|
||||||
showFb2Page(Math.floor(progress * (fb2TotalPages - 1)));
|
showFb2Page(Math.round(progress * (state.fb2TotalPages - 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== PUBLIC API ==========
|
// ========== PUBLIC API ==========
|
||||||
|
window.loadBookFromBase64 = function (base64Data, format, lastPosition) {
|
||||||
|
debugLog(`loadBookFromBase64: format=${format}, len=${base64Data ? base64Data.length : 0}`);
|
||||||
|
state.bookFormat = format;
|
||||||
|
|
||||||
|
if (format === 'epub') loadEpubFromBase64(base64Data, lastPosition);
|
||||||
|
else if (format === 'fb2') loadFb2FromBase64(base64Data, lastPosition);
|
||||||
|
else showError('Unsupported format: ' + format);
|
||||||
|
};
|
||||||
|
|
||||||
window.nextPage = function () {
|
window.nextPage = function () {
|
||||||
if (bookFormat === 'epub' && rendition) rendition.next();
|
if (state.bookFormat === 'epub' && state.rendition) {
|
||||||
else if (bookFormat === 'fb2' && fb2CurrentPage < fb2TotalPages - 1) {
|
state.rendition.next();
|
||||||
showFb2Page(fb2CurrentPage + 1);
|
} else if (state.bookFormat === 'fb2' && state.fb2CurrentPage < state.fb2TotalPages - 1) {
|
||||||
|
showFb2Page(state.fb2CurrentPage + 1);
|
||||||
updateFb2Progress();
|
updateFb2Progress();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.prevPage = function () {
|
window.prevPage = function () {
|
||||||
if (bookFormat === 'epub' && rendition) rendition.prev();
|
if (state.bookFormat === 'epub' && state.rendition) {
|
||||||
else if (bookFormat === 'fb2' && fb2CurrentPage > 0) {
|
state.rendition.prev();
|
||||||
showFb2Page(fb2CurrentPage - 1);
|
} else if (state.bookFormat === 'fb2' && state.fb2CurrentPage > 0) {
|
||||||
|
showFb2Page(state.fb2CurrentPage - 1);
|
||||||
updateFb2Progress();
|
updateFb2Progress();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.setFontSize = function (size) {
|
window.setFontSize = function (size) {
|
||||||
if (bookFormat === 'epub' && rendition) rendition.themes.fontSize(size + 'px');
|
if (state.bookFormat === 'epub' && state.rendition) {
|
||||||
else if (bookFormat === 'fb2') {
|
state.rendition.themes.fontSize(size + 'px');
|
||||||
var inner = document.getElementById('fb2-inner');
|
} else if (state.bookFormat === 'fb2') {
|
||||||
if (inner) { inner.style.fontSize = size + 'px'; setTimeout(setupFb2Pagination, 100); }
|
const inner = $('fb2-inner');
|
||||||
|
if (inner) {
|
||||||
|
inner.style.fontSize = size + 'px';
|
||||||
|
requestAnimationFrame(setupFb2Pagination);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.setFontFamily = function (family) {
|
window.setFontFamily = function (family) {
|
||||||
if (bookFormat === 'epub' && rendition) rendition.themes.font(family);
|
if (state.bookFormat === 'epub' && state.rendition) {
|
||||||
else if (bookFormat === 'fb2') {
|
state.rendition.themes.font(family);
|
||||||
var inner = document.getElementById('fb2-inner');
|
} else if (state.bookFormat === 'fb2') {
|
||||||
if (inner) { inner.style.fontFamily = family; setTimeout(setupFb2Pagination, 100); }
|
const inner = $('fb2-inner');
|
||||||
|
if (inner) {
|
||||||
|
inner.style.fontFamily = family;
|
||||||
|
requestAnimationFrame(setupFb2Pagination);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.goToChapter = function (href) {
|
window.goToChapter = function (href) {
|
||||||
if (bookFormat === 'epub' && rendition) rendition.display(href);
|
if (state.bookFormat === 'epub' && state.rendition) {
|
||||||
else if (bookFormat === 'fb2') {
|
state.rendition.display(href);
|
||||||
var inner = document.getElementById('fb2-inner');
|
} else if (state.bookFormat === 'fb2') {
|
||||||
var container = document.getElementById('fb2-content');
|
const inner = $('fb2-inner');
|
||||||
|
const container = els.fb2Content;
|
||||||
if (inner && container) {
|
if (inner && container) {
|
||||||
var el = inner.querySelector('[data-chapter="' + href + '"]');
|
const el = inner.querySelector(`[data-chapter="${href}"]`);
|
||||||
if (el) { showFb2Page(Math.floor(el.offsetLeft / container.clientWidth)); updateFb2Progress(); }
|
if (el) {
|
||||||
|
showFb2Page(Math.floor(el.offsetLeft / container.clientWidth));
|
||||||
|
updateFb2Progress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.getProgress = function () {
|
window.getProgress = function () {
|
||||||
if (bookFormat === 'epub' && book && currentCfi) {
|
if (state.bookFormat === 'epub' && state.book && state.currentCfi) {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify({ progress: book.locations.percentageFromCfi(currentCfi) || 0, cfi: currentCfi, currentPage: currentPage, totalPages: totalPages });
|
return JSON.stringify({
|
||||||
|
progress: state.book.locations.percentageFromCfi(state.currentCfi) || 0,
|
||||||
|
cfi: state.currentCfi,
|
||||||
|
currentPage: state.currentPage,
|
||||||
|
totalPages: state.totalPages
|
||||||
|
});
|
||||||
} catch (e) { return '{}'; }
|
} catch (e) { return '{}'; }
|
||||||
} else if (bookFormat === 'fb2') {
|
} else if (state.bookFormat === 'fb2') {
|
||||||
var p = fb2TotalPages > 1 ? fb2CurrentPage / (fb2TotalPages - 1) : 0;
|
const p = state.fb2TotalPages > 1 ? state.fb2CurrentPage / (state.fb2TotalPages - 1) : 0;
|
||||||
return JSON.stringify({ progress: p, cfi: p.toString(), currentPage: fb2CurrentPage + 1, totalPages: fb2TotalPages });
|
return JSON.stringify({
|
||||||
|
progress: p,
|
||||||
|
cfi: p.toString(),
|
||||||
|
currentPage: state.fb2CurrentPage + 1,
|
||||||
|
totalPages: state.fb2TotalPages
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return '{}';
|
return '{}';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== INIT ==========
|
// ========== INIT ==========
|
||||||
debugLog('Page loaded');
|
initTouchZones();
|
||||||
|
|
||||||
// Log hybridWebViewHost details
|
|
||||||
if (window.HybridWebView) {
|
|
||||||
debugLog('hybridWebView found!');
|
|
||||||
var methods = [];
|
|
||||||
for (var k in window.HybridWebView) {
|
|
||||||
methods.push(k + ':' + typeof window.HybridWebView[k]);
|
|
||||||
}
|
|
||||||
debugLog('Methods: ' + methods.join(', '));
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingText('Waiting for book...');
|
setLoadingText('Waiting for book...');
|
||||||
sendMessage('readerReady', {});
|
sendMessage('readerReady', {});
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- IMPORTANT: Load JSZip BEFORE epub.js -->
|
})();
|
||||||
<!-- JSZip 3.x is required by epub.js -->
|
|
||||||
<script>
|
|
||||||
// Verify after script load
|
|
||||||
debugLog('Checking libraries after inline script...');
|
|
||||||
</script>
|
|
||||||
<script src="js/jszip.min.js" onload="debugLog('JSZip loaded: ' + typeof JSZip)" onerror="debugLog('ERROR: jszip.min.js failed to load')"></script>
|
|
||||||
<script>
|
|
||||||
// After JSZip loads, verify it
|
|
||||||
if (typeof JSZip !== 'undefined') {
|
|
||||||
debugLog('JSZip OK, version: ' + (JSZip.version || 'unknown'));
|
|
||||||
} else {
|
|
||||||
debugLog('JSZip NOT available after script tag');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="js/epub.min.js" onload="debugLog('epub.js loaded: ' + typeof ePub)" onerror="debugLog('ERROR: epub.min.js failed to load')"></script>
|
|
||||||
<script>
|
|
||||||
if (typeof ePub !== 'undefined') {
|
|
||||||
debugLog('ePub OK');
|
|
||||||
} else {
|
|
||||||
debugLog('ePub NOT available after script tag');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -121,6 +121,9 @@ public partial class ReaderViewModel : BaseViewModel
|
|||||||
{
|
{
|
||||||
if (Book == null) return;
|
if (Book == null) return;
|
||||||
|
|
||||||
|
// Важно: если CFI пустой, не перезаписываем старый прогресс (защита от багов JS)
|
||||||
|
if (string.IsNullOrEmpty(cfi) && progress <= 0) return;
|
||||||
|
|
||||||
Book.ReadingProgress = progress;
|
Book.ReadingProgress = progress;
|
||||||
Book.LastCfi = cfi;
|
Book.LastCfi = cfi;
|
||||||
Book.LastChapter = chapter;
|
Book.LastChapter = chapter;
|
||||||
@@ -128,6 +131,7 @@ public partial class ReaderViewModel : BaseViewModel
|
|||||||
Book.TotalPages = totalPages;
|
Book.TotalPages = totalPages;
|
||||||
Book.LastRead = DateTime.UtcNow;
|
Book.LastRead = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Сохраняем в базу данных
|
||||||
await _databaseService.UpdateBookAsync(Book);
|
await _databaseService.UpdateBookAsync(Book);
|
||||||
|
|
||||||
await _databaseService.SaveProgressAsync(new ReadingProgress
|
await _databaseService.SaveProgressAsync(new ReadingProgress
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:BookReader.ViewModels"
|
xmlns:vm="clr-namespace:BookReader.ViewModels"
|
||||||
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||||
|
xmlns:converters="clr-namespace:BookReader.Converters"
|
||||||
x:Class="BookReader.Views.ReaderPage"
|
x:Class="BookReader.Views.ReaderPage"
|
||||||
x:DataType="vm:ReaderViewModel"
|
x:DataType="vm:ReaderViewModel"
|
||||||
Shell.NavBarIsVisible="False"
|
Shell.NavBarIsVisible="False"
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public partial class ReaderPage : ContentPage
|
|||||||
_viewModel = viewModel;
|
_viewModel = viewModel;
|
||||||
BindingContext = viewModel;
|
BindingContext = viewModel;
|
||||||
_viewModel.OnJavaScriptRequested += OnJavaScriptRequested;
|
_viewModel.OnJavaScriptRequested += OnJavaScriptRequested;
|
||||||
|
//Microsoft.Maui.Controls.Application.Current.Deactivated += async (s, e) =>
|
||||||
|
//{
|
||||||
|
// if (_isActive) await SaveCurrentProgress();
|
||||||
|
//};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
@@ -44,7 +48,13 @@ public partial class ReaderPage : ContentPage
|
|||||||
await SaveCurrentProgress();
|
await SaveCurrentProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async void OnNavigatedFrom(NavigatedFromEventArgs args)
|
||||||
|
{
|
||||||
|
_isActive = false;
|
||||||
|
base.OnNavigatedFrom(args);
|
||||||
|
// Сохраняем немедленно при любом уходе со страницы
|
||||||
|
await SaveCurrentProgress();
|
||||||
|
}
|
||||||
|
|
||||||
// ========== ЗАГРУЗКА КНИГИ ==========
|
// ========== ЗАГРУЗКА КНИГИ ==========
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user